jsonnet-bundler/pkg/packages.go

380 lines
9.7 KiB
Go
Raw Normal View History

// Copyright 2018 jsonnet-bundler authors
//
// Licensed 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.
package pkg
import (
"context"
"crypto/sha256"
"encoding/base64"
"fmt"
"io"
"os"
"path/filepath"
2020-01-24 08:02:34 +01:00
"strings"
"github.com/fatih/color"
"github.com/pkg/errors"
"github.com/jsonnet-bundler/jsonnet-bundler/pkg/jsonnetfile"
v1 "github.com/jsonnet-bundler/jsonnet-bundler/spec/v1"
"github.com/jsonnet-bundler/jsonnet-bundler/spec/v1/deps"
)
var (
VersionMismatch = errors.New("multiple colliding versions specified")
)
2020-01-24 08:02:34 +01:00
// Ensure receives all direct packages, the directory to vendor into and all known locks.
// It then makes sure all direct and nested dependencies are present in vendor at the correct version:
//
// If the package is locked and the files in vendor match the sha256 checksum,
// nothing needs to be done. Otherwise, the package is retrieved from the
// upstream source and added into vendor. If previously locked, the sums are
// checked as well.
// In case a (nested) package is already present in the lock,
// the one from the lock takes precedence. This allows the user to set the
// desired version in case by `jb install`ing it.
//
// Finally, all unknown files and directories are removed from vendor/
2020-01-24 08:02:34 +01:00
// The full list of locked depedencies is returned
func Ensure(direct v1.JsonnetFile, vendorDir string, oldLocks map[string]deps.Dependency) (map[string]deps.Dependency, error) {
// ensure all required files are in vendor
2020-01-24 08:02:34 +01:00
// This is the actual installation
Correct path resolution to nested local dependencies The tool failed to properly resolve nested local dependencies to jsonnet bundles in different directory trees. This arises because the installation command resolves and installs nested jsonnet local dependencies relative to the root jsonnetfile, rather than track and evaluate the installation path relative to the nested library's jsonnetfile. Consider a repository with multiple local jsonnet bundles in various directory trees, organised as follows (lockfiles elided for brevity): /top/of/tree |- lib/module_A |- jsonnetfile.json |- lib/module_B |- jsonnetfile.json |- src/root_module |- jsonnetfile.json The modules depend on each other as follows: ┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐ │ │ │ │ │ │ │ src/root_module │──>│ lib/module_A │──>│ lib/module_B │ │ │ │ │ │ │ └───────────────────┘ └───────────────────┘ └───────────────────┘ where X ──> Y indicates bundle X depends on bundle Y, expressed by adding a dependency of type local in bundle X's jsonnetfile.json, whose path is the relative path from bundle X to bundle Y in the directory structure. For example, src/root_module will express a local dependency on path ../lib/module_A to depend on library module A. Invoking jb install in src/root_module will result in an error: jb: error: failed to install packages: downloading: symlink destination path does not exist: %w: stat /top/of/tree/src/module_B: no such file or directory This occurs because jsonnet-bundler improperly attempts to resolve the nested dependency on library module B relative to the root module path, i.e. src/root_module. The correct behaviour is to perform such resolution relative to the depending module's jsonnetfile.json, i.e. relative to lib/module_A.
2021-12-28 19:13:48 +00:00
locks, err := ensure(direct.Dependencies, vendorDir, "", oldLocks)
if err != nil {
return nil, err
}
2020-01-24 08:02:34 +01:00
// remove unchanged legacyNames
CleanLegacyName(locks)
// find unknown dirs in vendor/
names := []string{}
err = filepath.Walk(vendorDir, func(path string, i os.FileInfo, err error) error {
if path == vendorDir {
return nil
}
if !i.IsDir() {
return nil
}
names = append(names, path)
return nil
})
// remove them
for _, dir := range names {
name, err := filepath.Rel(vendorDir, dir)
if err != nil {
return nil, err
}
2020-01-24 08:02:34 +01:00
if !known(locks, name) {
if err := os.RemoveAll(dir); err != nil {
return nil, err
}
2020-01-24 08:02:34 +01:00
if !strings.HasPrefix(name, ".tmp") {
color.Magenta("CLEAN %s", dir)
}
}
}
2020-01-24 08:02:34 +01:00
// remove all symlinks, optionally adding known ones back later if wished
if err := cleanLegacySymlinks(vendorDir, locks); err != nil {
return nil, err
}
if !direct.LegacyImports {
return locks, nil
}
if err := linkLegacy(vendorDir, locks); err != nil {
return nil, err
}
// return the final lockfile contents
2020-01-24 08:02:34 +01:00
return locks, nil
}
func CleanLegacyName(list map[string]deps.Dependency) {
for k, d := range list {
// unset if not changed by user
if d.LegacyNameCompat == d.Source.LegacyName() {
dep := list[k]
dep.LegacyNameCompat = ""
list[k] = dep
}
}
}
func cleanLegacySymlinks(vendorDir string, locks map[string]deps.Dependency) error {
// local packages need to be ignored
locals := map[string]bool{}
for _, d := range locks {
if d.Source.LocalSource == nil {
continue
}
locals[filepath.Join(vendorDir, d.Name())] = true
}
// remove all symlinks first
return filepath.Walk(vendorDir, func(path string, i os.FileInfo, err error) error {
if locals[path] {
return nil
}
if i.Mode()&os.ModeSymlink != 0 {
if err := os.Remove(path); err != nil {
return err
}
}
return nil
})
}
func linkLegacy(vendorDir string, locks map[string]deps.Dependency) error {
// create only the ones we want
for _, d := range locks {
// localSource still uses the relative style
if d.Source.LocalSource != nil {
continue
}
legacyName := filepath.Join(vendorDir, d.LegacyName())
2020-01-24 08:02:34 +01:00
pkgName := d.Name()
taken, err := checkLegacyNameTaken(legacyName, pkgName)
if err != nil {
fmt.Println(err)
continue
}
if taken {
continue
}
// create the symlink
if err := os.Symlink(
filepath.Join(pkgName),
filepath.Join(legacyName),
); err != nil {
return err
}
}
return nil
}
func checkLegacyNameTaken(legacyName string, pkgName string) (bool, error) {
fi, err := os.Lstat(legacyName)
if err != nil {
// does not exist: not taken
if os.IsNotExist(err) {
return false, nil
}
// a real error
return false, err
}
// is it a symlink?
if fi.Mode()&os.ModeSymlink != 0 {
s, err := os.Readlink(legacyName)
if err != nil {
return false, err
}
color.Yellow("WARN: cannot link '%s' to '%s', because package '%s' already uses that name. The absolute import still works\n", pkgName, legacyName, s)
return true, nil
}
// sth else
color.Yellow("WARN: cannot link '%s' to '%s', because the file/directory already exists. The absolute import still works.\n", pkgName, legacyName)
return true, nil
}
func known(deps map[string]deps.Dependency, p string) bool {
p = filepath.ToSlash(p)
2020-01-24 08:02:34 +01:00
for _, d := range deps {
k := filepath.ToSlash(d.Name())
2020-01-24 08:02:34 +01:00
if strings.HasPrefix(p, k) || strings.HasPrefix(k, p) {
return true
}
}
return false
}
Correct path resolution to nested local dependencies The tool failed to properly resolve nested local dependencies to jsonnet bundles in different directory trees. This arises because the installation command resolves and installs nested jsonnet local dependencies relative to the root jsonnetfile, rather than track and evaluate the installation path relative to the nested library's jsonnetfile. Consider a repository with multiple local jsonnet bundles in various directory trees, organised as follows (lockfiles elided for brevity): /top/of/tree |- lib/module_A |- jsonnetfile.json |- lib/module_B |- jsonnetfile.json |- src/root_module |- jsonnetfile.json The modules depend on each other as follows: ┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐ │ │ │ │ │ │ │ src/root_module │──>│ lib/module_A │──>│ lib/module_B │ │ │ │ │ │ │ └───────────────────┘ └───────────────────┘ └───────────────────┘ where X ──> Y indicates bundle X depends on bundle Y, expressed by adding a dependency of type local in bundle X's jsonnetfile.json, whose path is the relative path from bundle X to bundle Y in the directory structure. For example, src/root_module will express a local dependency on path ../lib/module_A to depend on library module A. Invoking jb install in src/root_module will result in an error: jb: error: failed to install packages: downloading: symlink destination path does not exist: %w: stat /top/of/tree/src/module_B: no such file or directory This occurs because jsonnet-bundler improperly attempts to resolve the nested dependency on library module B relative to the root module path, i.e. src/root_module. The correct behaviour is to perform such resolution relative to the depending module's jsonnetfile.json, i.e. relative to lib/module_A.
2021-12-28 19:13:48 +00:00
func ensure(direct map[string]deps.Dependency, vendorDir, pathToParentModule string, locks map[string]deps.Dependency) (map[string]deps.Dependency, error) {
2020-01-24 08:02:34 +01:00
deps := make(map[string]deps.Dependency)
for _, d := range direct {
2020-01-24 08:02:34 +01:00
l, present := locks[d.Name()]
// already locked and the integrity is intact
if present {
2020-01-24 08:02:34 +01:00
d.Version = locks[d.Name()].Version
if check(l, vendorDir) {
2020-01-24 08:02:34 +01:00
deps[d.Name()] = l
continue
}
}
2020-01-24 08:02:34 +01:00
expectedSum := locks[d.Name()].Sum
// either not present or not intact: download again
2020-01-24 08:02:34 +01:00
dir := filepath.Join(vendorDir, d.Name())
os.RemoveAll(dir)
Correct path resolution to nested local dependencies The tool failed to properly resolve nested local dependencies to jsonnet bundles in different directory trees. This arises because the installation command resolves and installs nested jsonnet local dependencies relative to the root jsonnetfile, rather than track and evaluate the installation path relative to the nested library's jsonnetfile. Consider a repository with multiple local jsonnet bundles in various directory trees, organised as follows (lockfiles elided for brevity): /top/of/tree |- lib/module_A |- jsonnetfile.json |- lib/module_B |- jsonnetfile.json |- src/root_module |- jsonnetfile.json The modules depend on each other as follows: ┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐ │ │ │ │ │ │ │ src/root_module │──>│ lib/module_A │──>│ lib/module_B │ │ │ │ │ │ │ └───────────────────┘ └───────────────────┘ └───────────────────┘ where X ──> Y indicates bundle X depends on bundle Y, expressed by adding a dependency of type local in bundle X's jsonnetfile.json, whose path is the relative path from bundle X to bundle Y in the directory structure. For example, src/root_module will express a local dependency on path ../lib/module_A to depend on library module A. Invoking jb install in src/root_module will result in an error: jb: error: failed to install packages: downloading: symlink destination path does not exist: %w: stat /top/of/tree/src/module_B: no such file or directory This occurs because jsonnet-bundler improperly attempts to resolve the nested dependency on library module B relative to the root module path, i.e. src/root_module. The correct behaviour is to perform such resolution relative to the depending module's jsonnetfile.json, i.e. relative to lib/module_A.
2021-12-28 19:13:48 +00:00
locked, err := download(d, vendorDir, pathToParentModule)
if err != nil {
return nil, errors.Wrap(err, "downloading")
}
if expectedSum != "" && locked.Sum != expectedSum {
2020-01-24 08:02:34 +01:00
return nil, fmt.Errorf("checksum mismatch for %s. Expected %s but got %s", d.Name(), expectedSum, locked.Sum)
}
2020-01-24 08:02:34 +01:00
deps[d.Name()] = *locked
// we settled on a new version, add it to the locks for recursion
2020-01-24 08:02:34 +01:00
locks[d.Name()] = *locked
}
for _, d := range deps {
if d.Single {
// skip dependencies that explicitely don't want nested ones installed
continue
}
2020-01-24 08:02:34 +01:00
f, err := jsonnetfile.Load(filepath.Join(vendorDir, d.Name(), jsonnetfile.File))
2018-05-13 06:55:13 -07:00
if err != nil {
if os.IsNotExist(err) {
continue
}
2018-05-13 06:55:13 -07:00
return nil, err
}
Correct path resolution to nested local dependencies The tool failed to properly resolve nested local dependencies to jsonnet bundles in different directory trees. This arises because the installation command resolves and installs nested jsonnet local dependencies relative to the root jsonnetfile, rather than track and evaluate the installation path relative to the nested library's jsonnetfile. Consider a repository with multiple local jsonnet bundles in various directory trees, organised as follows (lockfiles elided for brevity): /top/of/tree |- lib/module_A |- jsonnetfile.json |- lib/module_B |- jsonnetfile.json |- src/root_module |- jsonnetfile.json The modules depend on each other as follows: ┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐ │ │ │ │ │ │ │ src/root_module │──>│ lib/module_A │──>│ lib/module_B │ │ │ │ │ │ │ └───────────────────┘ └───────────────────┘ └───────────────────┘ where X ──> Y indicates bundle X depends on bundle Y, expressed by adding a dependency of type local in bundle X's jsonnetfile.json, whose path is the relative path from bundle X to bundle Y in the directory structure. For example, src/root_module will express a local dependency on path ../lib/module_A to depend on library module A. Invoking jb install in src/root_module will result in an error: jb: error: failed to install packages: downloading: symlink destination path does not exist: %w: stat /top/of/tree/src/module_B: no such file or directory This occurs because jsonnet-bundler improperly attempts to resolve the nested dependency on library module B relative to the root module path, i.e. src/root_module. The correct behaviour is to perform such resolution relative to the depending module's jsonnetfile.json, i.e. relative to lib/module_A.
2021-12-28 19:13:48 +00:00
absolutePath, err := filepath.EvalSymlinks(filepath.Join(vendorDir, d.Name()))
if err != nil {
return nil, err
}
nested, err := ensure(f.Dependencies, vendorDir, absolutePath, locks)
2018-05-13 06:55:13 -07:00
if err != nil {
return nil, err
}
for _, d := range nested {
2020-01-24 08:02:34 +01:00
if _, ok := deps[d.Name()]; !ok {
deps[d.Name()] = d
}
}
}
return deps, nil
}
// download retrieves a package from a remote upstream. The checksum of the
// files is generated afterwards.
Correct path resolution to nested local dependencies The tool failed to properly resolve nested local dependencies to jsonnet bundles in different directory trees. This arises because the installation command resolves and installs nested jsonnet local dependencies relative to the root jsonnetfile, rather than track and evaluate the installation path relative to the nested library's jsonnetfile. Consider a repository with multiple local jsonnet bundles in various directory trees, organised as follows (lockfiles elided for brevity): /top/of/tree |- lib/module_A |- jsonnetfile.json |- lib/module_B |- jsonnetfile.json |- src/root_module |- jsonnetfile.json The modules depend on each other as follows: ┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐ │ │ │ │ │ │ │ src/root_module │──>│ lib/module_A │──>│ lib/module_B │ │ │ │ │ │ │ └───────────────────┘ └───────────────────┘ └───────────────────┘ where X ──> Y indicates bundle X depends on bundle Y, expressed by adding a dependency of type local in bundle X's jsonnetfile.json, whose path is the relative path from bundle X to bundle Y in the directory structure. For example, src/root_module will express a local dependency on path ../lib/module_A to depend on library module A. Invoking jb install in src/root_module will result in an error: jb: error: failed to install packages: downloading: symlink destination path does not exist: %w: stat /top/of/tree/src/module_B: no such file or directory This occurs because jsonnet-bundler improperly attempts to resolve the nested dependency on library module B relative to the root module path, i.e. src/root_module. The correct behaviour is to perform such resolution relative to the depending module's jsonnetfile.json, i.e. relative to lib/module_A.
2021-12-28 19:13:48 +00:00
func download(d deps.Dependency, vendorDir, pathToParentModule string) (*deps.Dependency, error) {
var p Interface
switch {
case d.Source.GitSource != nil:
p = NewGitPackage(d.Source.GitSource)
case d.Source.LocalSource != nil:
Correct path resolution to nested local dependencies The tool failed to properly resolve nested local dependencies to jsonnet bundles in different directory trees. This arises because the installation command resolves and installs nested jsonnet local dependencies relative to the root jsonnetfile, rather than track and evaluate the installation path relative to the nested library's jsonnetfile. Consider a repository with multiple local jsonnet bundles in various directory trees, organised as follows (lockfiles elided for brevity): /top/of/tree |- lib/module_A |- jsonnetfile.json |- lib/module_B |- jsonnetfile.json |- src/root_module |- jsonnetfile.json The modules depend on each other as follows: ┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐ │ │ │ │ │ │ │ src/root_module │──>│ lib/module_A │──>│ lib/module_B │ │ │ │ │ │ │ └───────────────────┘ └───────────────────┘ └───────────────────┘ where X ──> Y indicates bundle X depends on bundle Y, expressed by adding a dependency of type local in bundle X's jsonnetfile.json, whose path is the relative path from bundle X to bundle Y in the directory structure. For example, src/root_module will express a local dependency on path ../lib/module_A to depend on library module A. Invoking jb install in src/root_module will result in an error: jb: error: failed to install packages: downloading: symlink destination path does not exist: %w: stat /top/of/tree/src/module_B: no such file or directory This occurs because jsonnet-bundler improperly attempts to resolve the nested dependency on library module B relative to the root module path, i.e. src/root_module. The correct behaviour is to perform such resolution relative to the depending module's jsonnetfile.json, i.e. relative to lib/module_A.
2021-12-28 19:13:48 +00:00
wd, err := os.Getwd()
if err != nil {
return nil, fmt.Errorf("failed to get current working directory: %w", err)
}
// Resolve the relative path to the parent module. When a local
// dependency tree is resolved recursively, nested local dependencies
// with relative paths must be evaluated relative to their referencing
// jsonnetfile, rather than relative to the top-level jsonnetfile.
modulePath, err := filepath.Rel(wd, filepath.Join(pathToParentModule, d.Source.LocalSource.Directory))
if err != nil {
modulePath = d.Source.LocalSource.Directory
}
p = NewLocalPackage(&deps.Local{Directory: modulePath})
case d.Source.HttpSource != nil:
p = NewHttpPackage(d.Source.HttpSource)
}
if p == nil {
return nil, errors.New("either git, local or http source is required")
}
2020-01-24 08:02:34 +01:00
version, err := p.Install(context.TODO(), d.Name(), vendorDir, d.Version)
if err != nil {
return nil, err
}
2019-10-30 17:38:26 +01:00
var sum string
if d.Source.LocalSource == nil {
2020-01-24 08:02:34 +01:00
sum = hashDir(filepath.Join(vendorDir, d.Name()))
2019-10-30 17:38:26 +01:00
}
2020-01-24 08:02:34 +01:00
d.Version = version
d.Sum = sum
return &d, nil
}
// check returns whether the files present at the vendor/ folder match the
2019-10-30 17:38:26 +01:00
// sha256 sum of the package. local-directory dependencies are not checked as
// their purpose is to change during development where integrity checking would
// be a hindrance.
2020-01-24 08:02:34 +01:00
func check(d deps.Dependency, vendorDir string) bool {
2019-10-30 17:38:26 +01:00
// assume a local dependency is intact as long as it exists
if d.Source.LocalSource != nil {
2020-01-24 08:02:34 +01:00
x, err := jsonnetfile.Exists(filepath.Join(vendorDir, d.Name()))
2019-10-30 17:38:26 +01:00
if err != nil {
return false
}
return x
}
if d.Sum == "" {
// no sum available, need to download
return false
2018-05-13 06:55:13 -07:00
}
2020-01-24 08:02:34 +01:00
dir := filepath.Join(vendorDir, d.Name())
sum := hashDir(dir)
return d.Sum == sum
}
// hashDir computes the checksum of a directory by concatenating all files and
// hashing this data using sha256. This can be memory heavy with lots of data,
// but jsonnet files should be fairly small
func hashDir(dir string) string {
hasher := sha256.New()
filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
if _, err := io.Copy(hasher, f); err != nil {
return err
}
return nil
})
return base64.StdEncoding.EncodeToString(hasher.Sum(nil))
}