pulumi/pkg/workspace/workspace.go
joeduffy 3164572b6e Fix some free variable capture logic
* Use `global.hasOwnProperty(ident)`, rather than `global[ident] !== undefined`,
  to avoid classifying references to globals as free variables.  Surprise(!!),
  the prior logic wouldn't work for `undefined` itself... 😒

* Expand this check to include the built-in Node.js module variables, namely
  `__dirname`, `__filename`, `exports`, `module`, and `require`, so that
  references to them don't get classified as serializable free variables either.

* Place catch variables in scope, so that `catch (err) { ... }` won't yield
  free variables for references to `err` within `...`.

* Place recursive function definitions into the top-level `var`-like scope of
  variables so that we don't consider references to them free.

* Harden all error pathways in the native C++ add-on so that we terminate
  anytime an exception is in-flight, rather than limping along and making
  things worse...
2017-09-05 15:21:14 -07:00

171 lines
5.2 KiB
Go

// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
package workspace
import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/golang/glog"
homedir "github.com/mitchellh/go-homedir"
"github.com/pulumi/pulumi-fabric/pkg/diag"
"github.com/pulumi/pulumi-fabric/pkg/encoding"
"github.com/pulumi/pulumi-fabric/pkg/tokens"
"github.com/pulumi/pulumi-fabric/pkg/util/contract"
)
// W offers functionality for interacting with Lumi workspaces. A workspace influences compilation; for example, it
// can specify default versions of dependencies, easing the process of working with multiple projects.
type W interface {
Path() string // the base path of the current workspace.
Root() string // the root path of the current workspace.
Settings() *Settings // returns a mutable pointer to the optional workspace settings info.
DetectPackage() (string, error) // locates the nearest project file in the directory hierarchy.
Save() error // saves any modifications to the workspace.
}
// New creates a new workspace from the given starting path.
func New(path string, diag diag.Sink) (W, error) {
contract.Requiref(diag != nil, "diag", "!= nil")
// First normalize the path to an absolute one.
var err error
path, err = filepath.Abs(path)
if err != nil {
return nil, err
}
home, err := homedir.Dir()
if err != nil {
return nil, err
}
ws := workspace{
diag: diag,
path: path,
home: home,
}
// Perform our I/O: memoize the root directory and load up any settings before returning.
if err := ws.init(); err != nil {
return nil, err
}
return &ws, nil
}
type workspace struct {
diag diag.Sink // the diagnostics sink to use for messages.
path string // the path at which the workspace was constructed.
home string // the home directory to use for this workspace.
root string // the root of the workspace.
settings Settings // an optional bag of workspace-wide settings.
}
// init finds the root of the workspace, caches it for fast lookups, and loads up any workspace settings.
func (w *workspace) init() error {
if w.root == "" {
// Detect the root of the workspace and cache it.
root := pathDir(w.path)
Search:
for {
files, err := ioutil.ReadDir(root)
if err != nil {
return err
}
for _, file := range files {
// A lumi directory delimits the root of the workspace.
lumidir := filepath.Join(root, file.Name())
if IsLumiDir(lumidir) {
glog.V(3).Infof("Lumi workspace detected; setting root to %v", root)
w.root = root // remember the root.
w.settings, err = w.readSettings() // load up optional settings.
if err != nil {
return err
}
break Search
}
}
// If neither succeeded, keep looking in our parent directory.
if root = filepath.Dir(root); isTop(root) {
// We reached the top of the filesystem. Just set root back to the path and stop.
glog.V(3).Infof("No Lumi workspace found; defaulting to current path %v", w.root)
w.root = w.path
break
}
}
}
return nil
}
func (w *workspace) Path() string { return w.path }
func (w *workspace) Root() string { return w.root }
func (w *workspace) Settings() *Settings { return &w.settings }
func (w *workspace) DetectPackage() (string, error) {
return DetectPackage(w.path, w.diag)
}
// qnamePath just cleans a name and makes sure it's appropriate to use as a path.
func qnamePath(nm tokens.QName) string {
return stringNamePath(string(nm))
}
// stringNamePart cleans a string component of a name and makes sure it's appropriate to use as a path.
func stringNamePath(nm string) string {
return strings.Replace(nm, tokens.QNameDelimiter, string(os.PathSeparator), -1)
}
// Save persists any in-memory changes made to the workspace.
func (w *workspace) Save() error {
// For now, the only changes to commit are the settings file changes.
return w.saveSettings()
}
// settingsFile returns the settings file location for this workspace.
func (w *workspace) settingsFile(ext string) string {
return filepath.Join(w.root, Dir, SettingsFile+ext)
}
// readSettings loads a settings file from the workspace, probing for all available extensions.
func (w *workspace) readSettings() (Settings, error) {
// Attempt to load the raw bytes from all available extensions.
var settings Settings
for _, ext := range encoding.Exts {
// See if the file exists.
path := w.settingsFile(ext)
b, err := ioutil.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
continue // try the next extension
}
return settings, err
}
// If it does, go ahead and decode it.
m := encoding.Marshalers[ext]
if err := m.Unmarshal(b, &settings); err != nil {
return settings, err
}
}
return settings, nil
}
// saveSettings saves the settings into a file for this workspace, committing any in-memory changes that have been made.
// IDEA: right now, we only support JSON. It'd be ideal if we supported YAML too (and it would be quite easy).
func (w *workspace) saveSettings() error {
m := encoding.Default()
settings := w.Settings()
b, err := m.Marshal(settings)
if err != nil {
return err
}
path := w.settingsFile(encoding.DefaultExt())
return ioutil.WriteFile(path, b, 0644)
}