pulumi/pkg/workspace/paths.go
joeduffy d100f77b9c Implement dependency resolution
This change includes logic to resolve dependencies declared by stacks.  The design
is described in https://github.com/marapongo/mu/blob/master/docs/deps.md.

In summary, each stack may declare dependencies, which are name/semver pairs.  A
new structure has been introduced, ast.Ref, to distinguish between ast.Names and
dependency names.  An ast.Ref includes a protocol, base part, and a name part (the
latter being an ast.Name); for example, in "https://hub.mu.com/mu/container/",
"https://" is the protocol, "hub.mu.com/" is the base, and "mu/container" is the
name.  This is used to resolve URL-like names to package manager-like artifacts.

The dependency resolution phase happens after parsing, but before semantic analysis.
This is because dependencies are "source-like" in that we must load and parse all
dependency metadata files.  We stick the full transitive closure of dependencies
into a map attached to the compiler to avoid loading dependencies multiple times.
Note that, although dependencies prohibit cycles, this forms a DAG, meaning multiple
inbound edges to a single stack may come from multiple places.

From there, we rely on ordinary visitation to deal with dependencies further.
This includes inserting symbol entries into the symbol table, mapping names to the
loaded stacks, during the first phase of binding so that they may be found
subsequently when typechecking during the second phase and beyond.
2016-11-21 11:19:25 -08:00

146 lines
4 KiB
Go

// Copyright 2016 Marapongo, Inc. All rights reserved.
package workspace
import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/marapongo/mu/pkg/diag"
"github.com/marapongo/mu/pkg/errors"
)
// Mufile is the base name of a Mufile.
const Mufile = "Mu"
// Muspace is a directory containing settings, modules, etc., delimiting a workspace.
const Muspace = ".mu"
// MuspaceStacks is the directory in which dependency modules exist, either local to a workspace, or globally.
const MuspaceStacks = "stacks"
// MuspaceWorkspace is the base name of a workspace settings file.
const MuspaceWorkspace = "workspace"
// Exts contains a list of all the valid Mufile and Mucluster extensions.
var Exts = []string{
".json",
".yaml",
// Although ".yml" is not a sanctioned YAML extension, it is used quite broadly; so we will support it.
".yml",
}
// InstallRootEnvvar is the envvar describing where Mu has been installed.
const InstallRootEnvvar = "MUROOT"
// InstallRootLibdir is the directory in which the standard Mu library exists.
const InstallRootLibdir = "lib"
// DefaultInstallRoot is where Mu is installed by default, if the envvar is missing.
// TODO: support Windows.
const DefaultInstallRoot = "/usr/lib/mu"
// InstallRoot returns Mu's installation location. This is controlled my the MUROOT envvar.
func InstallRoot() string {
root := os.Getenv(InstallRootEnvvar)
if root == "" {
return DefaultInstallRoot
}
return root
}
// isTop returns true if the path represents the top of the filesystem.
func isTop(path string) bool {
return os.IsPathSeparator(path[len(path)-1])
}
// pathDir returns the nearest directory to the given path (identity if a directory; parent otherwise).
func pathDir(path string) string {
// It's possible that the path is a file (e.g., a Mu.yaml file); if so, we want the directory.
info, err := os.Stat(path)
if err != nil || info.IsDir() {
return path
} else {
return filepath.Dir(path)
}
}
// DetectMufile locates the closest Mufile from the given path, searching "upwards" in the directory hierarchy. If no
// Mufile is found, an empty path is returned. If problems are detected, they are logged to the diag.Sink.
func DetectMufile(path string, d diag.Sink) (string, error) {
// It's possible the target is already the file we seek; if so, return right away.
if IsMufile(path, d) {
return path, nil
}
curr := pathDir(path)
for {
stop := false
// Enumerate the current path's files, checking each to see if it's a Mufile.
files, err := ioutil.ReadDir(curr)
if err != nil {
return "", err
}
for _, file := range files {
name := file.Name()
path := filepath.Join(curr, name)
if IsMufile(path, d) {
return path, nil
} else if name == Muspace {
// If we hit a .muspace file, stop looking.
stop = true
}
}
// If we encountered a stop condition, break out of the loop.
if stop {
break
}
// If neither succeeded, keep looking in our parent directory.
curr = filepath.Dir(curr)
if isTop(curr) {
break
}
}
return "", nil
}
// IsMufile returns true if the path references what appears to be a valid Mufile. If problems are detected -- like
// an incorrect extension -- they are logged to the provided diag.Sink (if non-nil).
func IsMufile(path string, d diag.Sink) bool {
info, err := os.Stat(path)
if err != nil || info.IsDir() {
// Missing files and directories can't be Mufiles.
return false
}
// Ensure the base name is expected.
name := info.Name()
ext := filepath.Ext(name)
base := strings.TrimSuffix(name, ext)
if base != Mufile {
if d != nil && strings.EqualFold(base, Mufile) {
// If the strings aren't equal, but case-insensitively match, issue a warning.
d.Warningf(errors.WarnIllegalMufileCasing.WithFile(name))
}
return false
}
// Check all supported extensions.
for _, mufileExt := range Exts {
if name == Mufile+mufileExt {
return true
}
}
// If we got here, it means the base name matched, but not the extension. Warn and return.
if d != nil {
d.Warningf(errors.WarnIllegalMufileExt.WithFile(name), ext)
}
return false
}