dep/ensure.go

391 строка
11 KiB
Go
Исходник Ответственный История

Этот файл содержит неоднозначные символы Юникода!

Этот файл содержит неоднозначные символы Юникода, которые могут быть перепутаны с другими в текущей локали. Если это намеренно, можете спокойно проигнорировать это предупреждение. Используйте кнопку Экранировать, чтобы подсветить эти символы.

// 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 main
import (
"bytes"
"encoding/hex"
"flag"
"fmt"
"io"
"log"
"os"
"path/filepath"
"strconv"
"strings"
"io/ioutil"
"github.com/pkg/errors"
"github.com/sdboyer/gps"
)
var ensureCmd = &command{
fn: runEnsure,
name: "ensure",
flag: flag.NewFlagSet("", flag.ExitOnError),
short: `[flags] <path>[:alt location][@<version specifier>]
To ensure a dependency is in your project at a specific version (if specified).
`,
long: `
Run it when
To ensure a new dependency is in your project.
To ensure a dependency is updated.
To the latest version that satisfies constraints.
To a specific version or different constraint.
(With no arguments) To ensure that you have all the dependencies specified by your Manifest + lockfile.
What it does
Download code, placing in the vendor/ directory of the project. Only the packages that are actually used by the current project and its dependencies are included.
Any authentication, proxy settings, or other parameters regarding communicating with external repositories is the responsibility of the underlying VCS tools.
Resolve version constraints
If the set of constraints are not solvable, print an error
Collapse any vendor folders in the downloaded code and its transient deps to the root.
Includes dependencies required by the current projects tests. There are arguments both for and against including the deps of tests for any transitive deps. Defer on deciding this for now.
Copy the relevant versions of the code to the current projects vendor directory.
The source of that code is implementation dependant. Probably some kind of local cache of the VCS data (not a GOPATH/workspace).
Write Manifest (if changed) and Lockfile
Print what changed
Flags:
-update update all packages
-n dry run
-override <specs> specify an override constraints for package(s)
Package specs:
<path>[:alt location][@<version specifier>]
Examples:
Fetch/update github.com/heroku/rollrus to latest version, including transitive dependencies (ensuring it matches the constraints of rollrus, or—if not contrained—their latest versions):
$ dep ensure github.com/heroku/rollrus
Same dep, but choose any minor patch release in the 0.9.X series, setting the constraint. If another constraint exists that constraint is changed to ~0.9.0:
$ dep ensure github.com/heroku/rollrus@~0.9.0
Same dep, but choose any release >= 0.9.1 and < 1.0.0, setting/changing constraints:
$ dep ensure github.com/heroku/rollrus@^0.9.1
Same dep, but updating to 1.0.X:
$ dep ensure github.com/heroku/rollrus@~1.0.0
Same dep, but fetching from a different location:
$ dep ensure github.com/heroku/rollrus:git.example.com/foo/bar
Same dep, but check out a specific version or range without updating the Manifest and update the Lockfile. This will fail if the specified version does not satisfy any existing constraints:
$ dep ensure github.com/heroku/rollrus==1.2.3 # 1.2.3 specifically
$ dep ensure github.com/heroku/rollrus=^1.2.0 # >= 1.2.0 < 2.0.0
Override any declared dependency range of 'github.com/foo/bar' to have the range of '^0.9.1'. This applies transitively:
$ dep ensure -override github.com/foo/bar@^0.9.1
Transitive deps are ensured based on constraints in the local Manifest if they exist, then constraints in the dependencys Manifest file. A lack of constraints defaults to the latest version, eg "^2".
For a description of the version specifier string, see this handy guide from crates.io. We are going to defer on making a final decision about this syntax until we have more experience with it in practice.
`,
}
// stringSlice is a slice of strings
type stringSlice []string
// implement the flag interface for stringSlice
func (s *stringSlice) String() string {
return fmt.Sprintf("%s", *s)
}
func (s *stringSlice) Set(value string) error {
*s = append(*s, value)
return nil
}
var overrides stringSlice
func init() {
ensureCmd.flag.Var(&overrides, "override", "Interpret specified constraint(s) as override(s) rather than normal constraints")
}
func runEnsure(args []string) error {
p, err := depContext.loadProject("")
if err != nil {
return err
}
sm, err := depContext.sourceManager()
if err != nil {
return err
}
defer sm.Release()
var errs []error
for _, arg := range args {
// default persist to manifest
constraint, err := getProjectConstraint(arg, sm)
if err != nil {
errs = append(errs, err)
}
p.m.Dependencies[constraint.Ident.ProjectRoot] = gps.ProjectProperties{
NetworkName: constraint.Ident.NetworkName,
Constraint: constraint.Constraint,
}
for i, lp := range p.l.P {
if lp.Ident() == constraint.Ident {
p.l.P = append(p.l.P[:i], p.l.P[i+1:]...)
break
}
}
}
for _, ovr := range overrides {
constraint, err := getProjectConstraint(ovr, sm)
if err != nil {
errs = append(errs, err)
}
p.m.Ovr[constraint.Ident.ProjectRoot] = gps.ProjectProperties{
NetworkName: constraint.Ident.NetworkName,
Constraint: constraint.Constraint,
}
for i, lp := range p.l.P {
if lp.Ident() == constraint.Ident {
p.l.P = append(p.l.P[:i], p.l.P[i+1:]...)
break
}
}
}
if len(errs) > 0 {
var buf bytes.Buffer
for err := range errs {
fmt.Fprintln(&buf, err)
}
return errors.New(buf.String())
}
params := gps.SolveParameters{
RootDir: p.absroot,
Manifest: p.m,
Lock: p.l,
Trace: true,
TraceLogger: log.New(os.Stdout, "", 0),
}
params.RootPackageTree, err = gps.ListPackages(p.absroot, string(p.importroot))
if err != nil {
return errors.Wrap(err, "ensure ListPackage for project")
}
solver, err := gps.Prepare(params, sm)
if err != nil {
return errors.Wrap(err, "ensure Prepare")
}
solution, err := solver.Solve()
if err != nil {
handleAllTheFailuresOfTheWorld(err)
return errors.Wrap(err, "ensure Solve()")
}
p.l.P = solution.Projects()
p.l.Memo = solution.InputHash()
tv, err := ioutil.TempDir("", "vendor")
if err != nil {
return errors.Wrap(err, "ensure making temporary vendor")
}
defer os.RemoveAll(tv)
tm, err := ioutil.TempFile("", "manifest")
if err != nil {
return errors.Wrap(err, "ensure making temporary manifest")
}
tm.Close()
defer os.Remove(tm.Name())
tl, err := ioutil.TempFile("", "lock")
if err != nil {
return errors.Wrap(err, "ensure making temporary lock file")
}
tl.Close()
defer os.Remove(tl.Name())
if err := gps.WriteDepTree(tv, p.l, sm, true); err != nil {
return errors.Wrap(err, "ensure gps.WriteDepTree")
}
if err := writeFile(tm.Name(), p.m); err != nil {
return errors.Wrap(err, "ensure writeFile for manifest")
}
if err := writeFile(tl.Name(), p.l); err != nil {
return errors.Wrap(err, "ensure writeFile for lock")
}
if err := os.Rename(tm.Name(), filepath.Join(p.absroot, manifestName)); err != nil {
return errors.Wrap(err, "ensure moving temp manifest into place!")
}
if err := os.Rename(tl.Name(), filepath.Join(p.absroot, lockName)); err != nil {
return errors.Wrap(err, "ensure moving temp manifest into place!")
}
os.RemoveAll(filepath.Join(p.absroot, "vendor"))
if err := copyFolder(tv, filepath.Join(p.absroot, "vendor")); err != nil {
return errors.Wrap(err, "ensure moving temp vendor")
}
return nil
}
func getProjectConstraint(arg string, sm *gps.SourceMgr) (gps.ProjectConstraint, error) {
constraint := gps.ProjectConstraint{}
// try to split on '@'
atIndex := strings.Index(arg, "@")
if atIndex > 0 {
parts := strings.SplitN(arg, "@", 2)
constraint.Constraint = deduceConstraint(parts[1])
arg = parts[0]
}
// TODO: What if there is no @, assume default branch (which may not be master) ?
// TODO: if we decide to keep equals.....
// split on colon if there is a network location
colonIndex := strings.Index(arg, ":")
if colonIndex > 0 {
parts := strings.SplitN(arg, ":", 2)
arg = parts[0]
constraint.Ident.NetworkName = parts[1]
}
pr, err := sm.DeduceProjectRoot(arg)
if err != nil {
return constraint, errors.Wrapf(err, "could not infer project root from dependency path: %s", arg) // this should go through to the user
}
if string(pr) != arg {
return constraint, errors.Wrapf(err, "dependency path %s is not a project root", arg)
}
constraint.Ident.ProjectRoot = gps.ProjectRoot(arg)
return constraint, nil
}
// deduceConstraint tries to puzzle out what kind of version is given in a string -
// semver, a revision, or as a fallback, a plain tag
func deduceConstraint(s string) gps.Constraint {
// always semver if we can
c, err := gps.NewSemverConstraint(s)
if err == nil {
return c
}
slen := len(s)
if slen == 40 {
if _, err = hex.DecodeString(s); err == nil {
// Whether or not it's intended to be a SHA1 digest, this is a
// valid byte sequence for that, so go with Revision. This
// covers git and hg
return gps.Revision(s)
}
}
// Next, try for bzr, which has a three-component GUID separated by
// dashes. There should be two, but the email part could contain
// internal dashes
if strings.Count(s, "-") >= 2 {
// Work from the back to avoid potential confusion from the email
i3 := strings.LastIndex(s, "-")
// Skip if - is last char, otherwise this would panic on bounds err
if slen == i3+1 {
return gps.NewVersion(s)
}
if _, err = hex.DecodeString(s[i3+1:]); err == nil {
i2 := strings.LastIndex(s[:i3], "-")
if _, err = strconv.ParseUint(s[i2+1:i3], 10, 64); err == nil {
// Getting this far means it'd pretty much be nuts if it's not a
// bzr rev, so don't bother parsing the email.
return gps.Revision(s)
}
}
}
// If not a plain SHA1 or bzr custom GUID, assume a plain version.
// TODO: if there is amgibuity here, then prompt the user?
return gps.NewVersion(s)
}
// copyFolder takes in a directory and copies it's contents to the destination.
// It preserves the file mode on files as well.
func copyFolder(src string, dest string) error {
fi, err := os.Lstat(src)
if err != nil {
return err
}
err = os.MkdirAll(dest, fi.Mode())
if err != nil {
return err
}
dir, err := os.Open(src)
if err != nil {
return err
}
defer dir.Close()
objects, err := dir.Readdir(-1)
if err != nil {
return err
}
for _, obj := range objects {
if obj.Mode()&os.ModeSymlink != 0 {
continue
}
srcfile := filepath.Join(src, obj.Name())
destfile := filepath.Join(dest, obj.Name())
if obj.IsDir() {
err = copyFolder(srcfile, destfile)
if err != nil {
return err
}
continue
}
if err := copyFile(srcfile, destfile); err != nil {
return err
}
}
return nil
}
// copyFile copies a file from one place to another with the permission bits
// perserved as well.
func copyFile(src string, dest string) error {
srcfile, err := os.Open(src)
if err != nil {
return err
}
defer srcfile.Close()
destfile, err := os.Create(dest)
if err != nil {
return err
}
defer destfile.Close()
if _, err := io.Copy(destfile, srcfile); err != nil {
return err
}
srcinfo, err := os.Stat(src)
if err != nil {
return err
}
return os.Chmod(dest, srcinfo.Mode())
}