Add basic targeting capability

This change partially implements pulumi/coconut#94, by adding the
ability to name targets during creation and reuse those names during
deletion and update.  This simplifies the management of deployment
records, checkpoints, and snapshots.

I've opted to call these things "husks" (perhaps going overboard with
joy after our recent renaming).  The basic idea is that for any
executable Nut that will be deployed, you have a nutpack/ directory
whose layout looks roughly as follows:

    nutpack/
        bin/
            Nutpack.json
            ... any other compiled artifacts ...
        husks/
            ... one snapshot per husk ...

For example, if we had a stage and prod husk, we would have:

    nutpack/
        bin/...
        husks/
            prod.json
            stage.json

In the prod.json and stage.json files, we'd have the most recent
deployment record for that environment.  These would presumably get
checked in and versioned along with the overall Nut, so that we
can use Git history for rollbacks, etc.

The create, update, and delete commands look in the right place for
these files automatically, so you don't need to manually supply them.
This commit is contained in:
joeduffy 2017-02-25 09:24:52 -08:00
parent 14762df98b
commit 977b16b2cc
15 changed files with 174 additions and 153 deletions

View file

@ -11,19 +11,20 @@ func newCreateCmd() *cobra.Command {
var summary bool
var output string
var cmd = &cobra.Command{
Use: "create [blueprint] [-- [args]]",
Short: "Create a new environment and its resources",
Long: "Create a new environment and its resources.\n" +
Use: "create husk-name [nut-file] [-- [args]]",
Short: "Create a new husk (target) with a given name and fresh resources",
Long: "Create a new husk (target) with a given name and fresh resources.\n" +
"\n" +
"This command creates a new environment and its resources. These resources are\n" +
"the result of compiling and evaluating a Nut blueprint, and then extracting all\n" +
"resource allocations from its CocoGL graph. This command results in a full snapshot\n" +
"of the environment's resource state, so that it may be updated incrementally later on.\n" +
"This command creates a new husk (target) and its resources, with the given name. These\n" +
"resources are computed by compiling and evaluating an executable Nut, and then extracting\n" +
"resource allocations from its resulting object graph. This command saves full snapshot\n" +
"of the husk's final resource state, so that it may be updated incrementally later on.\n" +
"\n" +
"By default, the Nut blueprint is loaded from the current directory. Optionally,\n" +
"a path to a Nut elsewhere can be provided as the [blueprint] argument.",
"By default, the Nut to execute is loaded from the current directory. Optionally, an\n" +
"explicit path can be provided using the [nut-file] argument.",
Run: func(cmd *cobra.Command, args []string) {
apply(cmd, args, "", applyOptions{
apply(cmd, args, applyOptions{
Create: true,
Delete: false,
DryRun: dryRun,
Summary: summary,
@ -40,7 +41,7 @@ func newCreateCmd() *cobra.Command {
"Only display summarization of resources and plan operations")
cmd.PersistentFlags().StringVarP(
&output, "output", "o", "",
"Serialize the resulting snapshot to a specific file, instead of the standard location")
"Serialize the resulting husk snapshot to a specific file, instead of the standard location")
return cmd
}

View file

@ -10,14 +10,19 @@ func newDeleteCmd() *cobra.Command {
var dryRun bool
var summary bool
var cmd = &cobra.Command{
Use: "delete [snapshot]",
Short: "Delete an existing environment and its resources",
Long: "Delete an existing environment and its resources.\n" +
Use: "delete husk-name",
Short: "Delete an existing husk (target) and its resources",
Long: "Delete an existing husk (target) and its resources.\n" +
"\n" +
"This command deletes an entire existing environment whose state is represented by the\n" +
"existing snapshot file. After running to completion, this environment will be gone.",
"This command deletes an entire existing husk by name. The current state is loaded\n" +
"from the associated snapshot file in the workspace. After running to completion,\n" +
"this environment and all of its associated state will be gone.\n" +
"\n" +
"Warning: although old snapshots can be used to recreate an environment, this command\n" +
"is generally irreversable and should be used with great care.",
Run: func(cmd *cobra.Command, args []string) {
applyExisting(cmd, args, applyOptions{
apply(cmd, args, applyOptions{
Create: false,
Delete: true,
DryRun: dryRun,
Summary: summary,

View file

@ -4,6 +4,7 @@ package cmd
import (
"fmt"
"os"
"strings"
"unicode"
@ -14,6 +15,7 @@ import (
"github.com/pulumi/coconut/pkg/tokens"
"github.com/pulumi/coconut/pkg/util/cmdutil"
"github.com/pulumi/coconut/pkg/util/contract"
"github.com/pulumi/coconut/pkg/workspace"
)
func newDescribeCmd() *cobra.Command {
@ -22,8 +24,8 @@ func newDescribeCmd() *cobra.Command {
var printSymbols bool
var printExportedSymbols bool
var cmd = &cobra.Command{
Use: "describe [packages...]",
Short: "Describe a Nut",
Use: "describe [nuts...]",
Short: "Describe one or more Nuts",
Long: "Describe prints package, symbol, and IL information from one or more Nuts.",
Run: func(cmd *cobra.Command, args []string) {
// If printAll is true, flip all the flags.
@ -33,13 +35,27 @@ func newDescribeCmd() *cobra.Command {
printExportedSymbols = true
}
// Enumerate the list of packages, deserialize them, and print information.
for _, arg := range args {
pkg := cmdutil.ReadPackageFromArg(arg)
if pkg == nil {
break
if len(args) == 0 {
// No package specified, just load from the current directory.
pwd, _ := os.Getwd()
pkgpath, err := workspace.DetectPackage(pwd, sink())
if err != nil {
fmt.Fprintf(os.Stderr, "fatal: could not find a nut: %v", err)
os.Exit(-1)
}
if pkg := cmdutil.ReadPackage(pkgpath); pkg != nil {
printPackage(pkg, printSymbols, printExportedSymbols, printIL)
}
} else {
// Enumerate the list of packages, deserialize them, and print information.
for _, arg := range args {
pkg := cmdutil.ReadPackageFromArg(arg)
if pkg == nil {
break
}
printPackage(pkg, printSymbols, printExportedSymbols, printIL)
}
printPackage(pkg, printSymbols, printExportedSymbols, printIL)
}
},
}

View file

@ -95,14 +95,14 @@ type compileResult struct {
}
// plan just uses the standard logic to parse arguments, options, and to create a snapshot and plan.
func plan(cmd *cobra.Command, args []string, existfn string, delete bool) *planResult {
func plan(cmd *cobra.Command, args []string, husk tokens.QName, create bool, delete bool) *planResult {
// Create a new context for the plan operations.
ctx := resource.NewContext(sink())
// If we are using an existing snapshot, read in that file (bailing if an IO error occurs).
var existing resource.Snapshot
if existfn != "" {
if existing = readSnapshot(ctx, existfn); existing == nil {
var old resource.Snapshot
if !create {
if old = readSnapshot(ctx, husk); old == nil {
return nil
}
}
@ -112,15 +112,14 @@ func plan(cmd *cobra.Command, args []string, existfn string, delete bool) *planR
return &planResult{
compileResult: nil,
Ctx: ctx,
Nutpoint: existfn,
Existing: existing,
Snap: nil,
Plan: resource.NewDeletePlan(ctx, existing),
Husk: husk,
Old: old,
New: nil,
Plan: resource.NewDeletePlan(ctx, old),
}
} else if result := compile(cmd, args); result != nil && result.Heap != nil {
// Create a resource snapshot from the compiled/evaluated object graph.
ns := resource.Namespace("no_namespace") // TODO[pulumi/coconut#94]: support for targets/namespaces.
snap, err := resource.NewGraphSnapshot(ctx, ns, result.Pkg.Name, result.C.Ctx().Opts.Args, result.Heap)
snap, err := resource.NewGraphSnapshot(ctx, husk, result.Pkg.Name, result.C.Ctx().Opts.Args, result.Heap)
if err != nil {
result.C.Diag().Errorf(errors.ErrorCantCreateSnapshot, err)
return nil
@ -129,19 +128,20 @@ func plan(cmd *cobra.Command, args []string, existfn string, delete bool) *planR
}
var plan resource.Plan
if existing == nil {
if create {
// Generate a plan for creating the resources from scratch.
plan = resource.NewCreatePlan(ctx, snap)
} else {
// Generate a plan for updating existing resources to the new snapshot.
plan = resource.NewUpdatePlan(ctx, existing, snap)
contract.Assert(old != nil)
plan = resource.NewUpdatePlan(ctx, old, snap)
}
return &planResult{
compileResult: result,
Ctx: ctx,
Nutpoint: existfn,
Existing: existing,
Snap: snap,
Husk: husk,
Old: old,
New: snap,
Plan: plan,
}
}
@ -151,15 +151,25 @@ func plan(cmd *cobra.Command, args []string, existfn string, delete bool) *planR
type planResult struct {
*compileResult
Ctx *resource.Context
Nutpoint string // the file from which the existing snapshot was loaded (if any).
Existing resource.Snapshot // the existing snapshot (if any).
Snap resource.Snapshot // the new snapshot for this plan (if any).
Plan resource.Plan
Ctx *resource.Context
Husk tokens.QName // the husk name.
Old resource.Snapshot // the existing snapshot (if any).
New resource.Snapshot // the new snapshot for this plan (if any).
Plan resource.Plan
}
func apply(cmd *cobra.Command, args []string, existing string, opts applyOptions) {
if result := plan(cmd, args, existing, opts.Delete); result != nil {
func apply(cmd *cobra.Command, args []string, opts applyOptions) {
// Read in the name of the husk to use.
var husk tokens.QName
if len(args) == 0 {
fmt.Fprintf(os.Stderr, "fatal: missing required husk name\n")
os.Exit(-1)
} else {
husk = tokens.QName(args[0])
args = args[1:]
}
if result := plan(cmd, args, husk, opts.Create, opts.Delete); result != nil {
if result.Plan.Empty() {
sink().Infof(diag.Message("nothing to do -- resources are up to date"))
} else if opts.DryRun {
@ -167,7 +177,7 @@ func apply(cmd *cobra.Command, args []string, existing string, opts applyOptions
if opts.Output == "" || opts.Output == "-" {
printPlan(result.Plan, opts.Summary)
} else {
saveSnapshot(result.Snap, opts.Output)
saveSnapshot(husk, result.New, opts.Output)
}
} else {
// Create an object to track progress and perform the actual operations.
@ -206,53 +216,38 @@ func apply(cmd *cobra.Command, args []string, existing string, opts applyOptions
fmt.Printf(colors.Colorize(s))
// Now save the updated snapshot to the specified output file, if any, or the standard location otherwise.
// TODO: perform partial updates if we weren't able to perform the entire planned set of operations.
// TODO: save partial updates if we weren't able to perform the entire planned set of operations.
if opts.Delete {
contract.Assert(result.Nutpoint != "")
deleteSnapshot(result.Nutpoint)
deleteSnapshot(result.Husk)
} else {
out := opts.Output
if out == "" {
out = result.Nutpoint // try overwriting the existing file.
}
if out == "" {
out = workspace.Nutpoint // use the default file name.
}
contract.Assert(result.Snap != nil)
saveSnapshot(result.Snap, out)
contract.Assert(result.New != nil)
saveSnapshot(result.Husk, result.New, opts.Output)
}
}
}
}
func applyExisting(cmd *cobra.Command, args []string, opts applyOptions) {
// Read in the snapshot argument.
// TODO: if not supplied, auto-detect the current one.
if len(args) == 0 {
fmt.Fprintf(os.Stderr, "fatal: missing required snapshot argument\n")
os.Exit(-1)
}
apply(cmd, args[1:], args[0], opts)
}
// backupSnapshot makes a backup of an existing file, in preparation for writing a new one. Instead of a copy, it
// simply renames the file, which is simpler, more efficient, etc.
func backupSnapshot(file string) {
contract.Require(file != "", "file")
// TODO: consider multiple backups (.bak.bak.bak...etc).
os.Rename(file, file+".bak") // ignore errors.
// TODO: consider multiple backups (.bak.bak.bak...etc).
}
// deleteSnapshot removes an existing snapshot file, leaving behind a backup.
func deleteSnapshot(file string) {
contract.Require(file != "", "file")
func deleteSnapshot(husk tokens.QName) {
contract.Require(husk != "", "husk")
// Just make a backup of the file and don't write out anything new.
file := workspace.HuskPath(husk)
backupSnapshot(file)
}
// readSnapshot reads in an existing snapshot file, issuing an error and returning nil if something goes awry.
func readSnapshot(ctx *resource.Context, file string) resource.Snapshot {
func readSnapshot(ctx *resource.Context, husk tokens.QName) resource.Snapshot {
contract.Require(husk != "", "husk")
file := workspace.HuskPath(husk)
// Detect the encoding of the file so we can do our initial unmarshaling.
m, ext := encoding.Detect(file)
if m == nil {
@ -291,10 +286,13 @@ func readSnapshot(ctx *resource.Context, file string) resource.Snapshot {
return resource.DeserializeSnapshot(ctx, &snap)
}
// saveSnapshot saves a new CocoGL snapshot at the given location, backing up any existing ones.
func saveSnapshot(snap resource.Snapshot, file string) {
// saveSnapshot saves a new snapshot at the given location, backing up any existing ones.
func saveSnapshot(husk tokens.QName, snap resource.Snapshot, file string) {
contract.Require(snap != nil, "snap")
contract.Require(file != "", "file")
contract.Require(husk != "", "husk")
if file == "" {
file = workspace.HuskPath(husk)
}
// Make a serializable CocoGL data structure and then use the encoder to encode it.
m, ext := encoding.Detect(file)
@ -313,15 +311,21 @@ func saveSnapshot(snap resource.Snapshot, file string) {
// Back up the existing file if it already exists.
backupSnapshot(file)
// And now write out the new snapshot file, overwriting that location.
if err = ioutil.WriteFile(file, b, 0644); err != nil {
// Ensure the directory exists.
if err = os.MkdirAll(filepath.Dir(file), 0744); err != nil {
sink().Errorf(errors.ErrorIO, err)
} else {
// And now write out the new snapshot file, overwriting that location.
if err = ioutil.WriteFile(file, b, 0644); err != nil {
sink().Errorf(errors.ErrorIO, err)
}
}
}
}
}
type applyOptions struct {
Create bool // true if we are creating resources.
Delete bool // true if we are deleting resources.
DryRun bool // true if we should just print the plan without performing it.
Summary bool // true if we should only summarize resources and operations.

View file

@ -11,21 +11,22 @@ func newUpdateCmd() *cobra.Command {
var summary bool
var output string
var cmd = &cobra.Command{
Use: "update [snapshot] [blueprint] [-- [args]]",
Short: "Update an existing environment and its resources",
Long: "Update an existing environment and its resources.\n" +
Use: "update husk-name [nut-file] [-- [args]]",
Short: "Update an existing husk (target) and its resources",
Long: "Update an existing husk (target) and its resources.\n" +
"\n" +
"This command updates an existing environment whose state is represented by the\n" +
"This command updates an existing husk environment whose state is represented by the\n" +
"existing snapshot file. The new desired state is computed by compiling and evaluating\n" +
"a Nut blueprint, and extracting all resource allocations from its CocoGL graph.\n" +
"This is then compared against the existing state to determine what operations must take\n" +
"an executable Nut, and extracting all resource allocations from its resulting object graph.\n" +
"This graph is compared against the existing state to determine what operations must take\n" +
"place to achieve the desired state. This command results in a full snapshot of the\n" +
"environment's new resource state, so that it may be updated incrementally again later.\n" +
"\n" +
"By default, the Nut blueprint is loaded from the current directory. Optionally,\n" +
"a path to a Nut elsewhere can be provided as the [blueprint] argument.",
"By default, the Nut to execute is loaded from the current directory. Optionally, an\n" +
"explicit path can be provided using the [nut-file] argument.",
Run: func(cmd *cobra.Command, args []string) {
applyExisting(cmd, args, applyOptions{
apply(cmd, args, applyOptions{
Create: false,
Delete: false,
DryRun: dryRun,
Summary: summary,
@ -42,7 +43,7 @@ func newUpdateCmd() *cobra.Command {
"Only display summarization of resources and plan operations")
cmd.PersistentFlags().StringVarP(
&output, "output", "o", "",
"Serialize the resulting snapshot to a specific file, instead of overwriting the existing one")
"Serialize the resulting husk snapshot to a specific file, instead of overwriting the existing one")
return cmd
}

View file

@ -1,3 +1,4 @@
bin/
node_modules/
nutpack/

View file

@ -1,2 +0,0 @@
/bin/

View file

@ -1,6 +0,0 @@
# ec2instance Mu Package
This is the auto-generated directory structure for the "ec2instance" Mu package.
The compiled package can be found underneath `bin/`, while all known deployment targets are in `targets/`.

View file

@ -1,2 +0,0 @@
/*.bak

View file

@ -1,6 +1,6 @@
{
"compilerOptions": {
"outDir": "bin",
"outDir": "nutpack/bin",
"target": "es6",
"module": "commonjs",
"moduleResolution": "node",

View file

@ -30,7 +30,7 @@ type Moniker string
const MonikerDelimiter = "::" // the delimiter between elements of the moniker.
// NewMoniker creates a unique moniker for the given object.
func NewMoniker(ns Namespace, alloc tokens.Module, t tokens.Type, name tokens.QName) Moniker {
func NewMoniker(ns tokens.QName, alloc tokens.Module, t tokens.Type, name tokens.QName) Moniker {
return Moniker(
string(ns) +
MonikerDelimiter + string(alloc) +

View file

@ -15,7 +15,7 @@ import (
// SerializedSnapshot is a serializable, flattened CocoGL graph structure, specifically for snapshots. It is similar
// to the actual Snapshot interface, except that it flattens and rearranges a few data structures for serializability.
type SerializedSnapshot struct {
Target Namespace `json:"target"` // the target environment name.
Husk tokens.QName `json:"husk"` // the target environment name.
Package tokens.PackageName `json:"package"` // the package which created this graph.
Args *core.Args `json:"args,omitempty"` // the blueprint args for graph creation.
Refs *string `json:"refs,omitempty"` // the ref alias, if any (`#ref` by default).
@ -66,7 +66,7 @@ func SerializeSnapshot(snap Snapshot, reftag string) *SerializedSnapshot {
}
return &SerializedSnapshot{
Target: snap.Ns(),
Husk: snap.Husk(),
Package: snap.Pkg(), // TODO: eventually, this should carry version metadata too.
Args: argsp,
Refs: refp,
@ -149,20 +149,20 @@ func SerializeProperty(prop PropertyValue, reftag string) (interface{}, bool) {
}
// DeserializeSnapshot takes a serialized CocoGL snapshot data structure and returns its associated snapshot.
func DeserializeSnapshot(ctx *Context, mugl *SerializedSnapshot) Snapshot {
func DeserializeSnapshot(ctx *Context, ser *SerializedSnapshot) Snapshot {
// Determine the reftag to use.
var reftag string
if mugl.Refs == nil {
if ser.Refs == nil {
reftag = DefaultSnapshotReftag
} else {
reftag = *mugl.Refs
reftag = *ser.Refs
}
// For every serialized resource vertex, create a SerializedResource out of it.
var resources []Resource
if mugl.Resources != nil {
if ser.Resources != nil {
// TODO: we need to enumerate resources in the specific order in which they were emitted.
for _, kvp := range mugl.Resources.Iter() {
for _, kvp := range ser.Resources.Iter() {
// Deserialize the resources, if they exist.
res := kvp.Value
var props PropertyMap
@ -183,11 +183,11 @@ func DeserializeSnapshot(ctx *Context, mugl *SerializedSnapshot) Snapshot {
}
var args core.Args
if mugl.Args != nil {
args = *mugl.Args
if ser.Args != nil {
args = *ser.Args
}
return NewSnapshot(ctx, mugl.Target, mugl.Package, args, resources)
return NewSnapshot(ctx, ser.Husk, ser.Package, args, resources)
}
func DeserializeProperties(props SerializedPropertyMap, reftag string) PropertyMap {

View file

@ -14,14 +14,12 @@ import (
"github.com/pulumi/coconut/pkg/util/contract"
)
type Namespace tokens.QName // a namespace is the target for a deployment.
// Snapshot is a view of a collection of resources in an environment at a point in time. It describes resources; their
// IDs, names, and properties; their dependencies; and more. A snapshot is a diffable entity and can be used to create
// or apply an infrastructure deployment plan in order to make reality match the snapshot state.
type Snapshot interface {
Ctx() *Context // fetches the context for this snapshot.
Ns() Namespace // the namespace being deployed into.
Husk() tokens.QName // the husk/namespace target being deployed into.
Pkg() tokens.PackageName // the package from which this snapshot came.
Args() core.Args // the arguments used to compile this package.
Resources() []Resource // a topologically sorted list of resources (based on dependencies).
@ -32,14 +30,15 @@ type Snapshot interface {
// NewSnapshot creates a snapshot from the given arguments. Note that resources must be in topologically-sorted
// dependency order, otherwise undefined behavior will result from using the resulting snapshot object.
func NewSnapshot(ctx *Context, ns Namespace, pkg tokens.PackageName, args core.Args, resources []Resource) Snapshot {
return &snapshot{ctx, ns, pkg, args, resources}
func NewSnapshot(ctx *Context, husk tokens.QName, pkg tokens.PackageName,
args core.Args, resources []Resource) Snapshot {
return &snapshot{ctx, husk, pkg, args, resources}
}
// NewGraphSnapshot takes an object graph and produces a resource snapshot from it. It understands how to name
// resources based on their position within the graph and how to identify and record dependencies. This function can
// fail dynamically if the input graph did not satisfy the preconditions for resource graphs (like that it is a DAG).
func NewGraphSnapshot(ctx *Context, ns Namespace, pkg tokens.PackageName, args core.Args,
func NewGraphSnapshot(ctx *Context, husk tokens.QName, pkg tokens.PackageName, args core.Args,
heap *heapstate.Heap) (Snapshot, error) {
// Topologically sort the entire heapstate (in dependency order) and extract just the resource objects.
@ -50,24 +49,24 @@ func NewGraphSnapshot(ctx *Context, ns Namespace, pkg tokens.PackageName, args c
// Next, name all resources, create their monikers and objects, and maps that we will use. Note that we must do
// this in DAG order (guaranteed by our topological sort above), so that referenced monikers are available.
resources, err := createResources(ctx, ns, heap, resobjs)
resources, err := createResources(ctx, husk, heap, resobjs)
if err != nil {
return nil, err
}
return NewSnapshot(ctx, ns, pkg, args, resources), nil
return NewSnapshot(ctx, husk, pkg, args, resources), nil
}
type snapshot struct {
ctx *Context // the context shared by all operations in this snapshot.
ns Namespace // the namespace being deployed into.
husk tokens.QName // the husk/namespace target being deployed into.
pkg tokens.PackageName // the package from which this snapshot came.
args core.Args // the arguments used to compile this package.
resources []Resource // the topologically sorted linearized list of resources.
}
func (s *snapshot) Ctx() *Context { return s.ctx }
func (s *snapshot) Ns() Namespace { return s.ns }
func (s *snapshot) Husk() tokens.QName { return s.husk }
func (s *snapshot) Pkg() tokens.PackageName { return s.pkg }
func (s *snapshot) Args() core.Args { return s.args }
func (s *snapshot) Resources() []Resource { return s.resources }
@ -82,7 +81,7 @@ func (s *snapshot) ResourceByObject(obj *rt.Object) Resource { return s.ctx.ObjR
// createResources uses a graph to create monikers and resource objects for every resource within. It
// returns two maps for further use: a map of vertex to its new resource object, and a map of vertex to its moniker.
func createResources(ctx *Context, ns Namespace, heap *heapstate.Heap, resobjs []*rt.Object) ([]Resource, error) {
func createResources(ctx *Context, husk tokens.QName, heap *heapstate.Heap, resobjs []*rt.Object) ([]Resource, error) {
var resources []Resource
for _, resobj := range resobjs {
// Create an object resource without a moniker.
@ -101,7 +100,7 @@ func createResources(ctx *Context, ns Namespace, heap *heapstate.Heap, resobjs [
// Now compute a unique moniker for this object and ensure we haven't had any collisions.
alloc := heap.Alloc(resobj)
moniker := NewMoniker(ns, alloc.Mod.Tok, t, name)
moniker := NewMoniker(husk, alloc.Mod.Tok, t, name)
glog.V(7).Infof("Resource moniker computed: %v", moniker)
if _, exists := ctx.MksRes[moniker]; exists {
// If this moniker is already in use, issue an error, ignore this one, and break. The break is necessary

View file

@ -11,35 +11,24 @@ import (
"github.com/pulumi/coconut/pkg/compiler/errors"
"github.com/pulumi/coconut/pkg/diag"
"github.com/pulumi/coconut/pkg/encoding"
"github.com/pulumi/coconut/pkg/tokens"
)
// Nutfile is the base name of a Nutfile.
const Nutfile = "Nut"
const Nutfile = "Nut" // the base name of a Nutfile.
const Nutpack = "Nutpack" // the base name of a compiled NutPack.
const NutpackOutDir = "nutpack" // the default name of the NutPack output directory.
const NutpackBinDir = "bin" // the default name of the NutPack binary output directory.
const NutpackHusksDir = "husks" // the default name of the NutPack husks directory.
const Nutspace = "Coconut" // the base name of a markup file for shared settings in a workspace.
const Nutdeps = ".Nuts" // the directory in which dependencies exist, either local or global.
// Nutpack is the base name of a compiled Nut package.
const Nutpack = "Nutpack"
// Nutpoint is the base name of a Nut's CocoGL graph file (checkpoint).
const Nutpoint = "Nutpoint"
// Nutspace is the base name of a markup file containing settings shared amongst a workspace.
const Nutspace = "Nutspace"
// Nutdeps is the directory in which dependency modules exist, either local to a workspace, or globally.
const Nutdeps = ".Nuts"
// InstallRootEnvvar is the envvar describing where Coconut has been installed.
const InstallRootEnvvar = "COCOROOT"
// InstallRootLibdir is the directory in which the Coconut standard library exists.
const InstallRootLibdir = "lib"
// DefaultInstallRoot is where Coconut is installed by default, if the envvar is missing.
// TODO: support Windows.
const DefaultInstallRoot = "/usr/local/coconut"
const InstallRootEnvvar = "COCOROOT" // the envvar describing where Coconut has been installed.
const InstallRootLibdir = "lib" // the directory in which the Coconut standard library exists.
const DefaultInstallRoot = "/usr/local/coconut" // where Coconut is installed by default.
// InstallRoot returns Coconut's installation location. This is controlled my the COCOROOT envvar.
func InstallRoot() string {
// TODO: support Windows.
root := os.Getenv(InstallRootEnvvar)
if root == "" {
return DefaultInstallRoot
@ -47,6 +36,11 @@ func InstallRoot() string {
return root
}
// HuskPath returns a path to the given husk's default location.
func HuskPath(husk tokens.QName) string {
return filepath.Join(NutpackOutDir, NutpackHusksDir, qnamePath(husk)+encoding.Exts[0])
}
// isTop returns true if the path represents the top of the filesystem.
func isTop(path string) bool {
return os.IsPathSeparator(path[len(path)-1])
@ -79,6 +73,17 @@ func DetectPackage(path string, d diag.Sink) (string, error) {
if err != nil {
return "", err
}
// See if there's a compiled Nutpack in the expected location.
pack := filepath.Join(NutpackOutDir, NutpackBinDir, Nutpack)
for _, ext := range encoding.Exts {
packfile := pack + ext
if IsNutpack(packfile, d) {
return packfile, nil
}
}
// Now look for individual Nutfiles.
for _, file := range files {
name := file.Name()
path := filepath.Join(curr, name)
@ -117,12 +122,6 @@ func IsNutpack(path string, d diag.Sink) bool {
return isMarkupFile(path, Nutpack, d)
}
// IsNutpoint returns true if the path references what appears to be a valid CocoGL file. If problems are detected --
// like an incorrect extension -- they are logged to the provided diag.Sink (if non-nil).
func IsNutpoint(path string, d diag.Sink) bool {
return isMarkupFile(path, Nutpoint, d)
}
// IsNutspace returns true if the path references what appears to be a valid Nutspace file. If problems are detected --
// like an incorrect extension -- they are logged to the provided diag.Sink (if non-nil).
func IsNutspace(path string, d diag.Sink) bool {

View file

@ -183,6 +183,11 @@ func namePath(nm tokens.Name) string {
return stringNamePath(string(nm))
}
// 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))
}
// packageNamePath just cleans a package name and makes sure it's appropriate to use as a path.
func packageNamePath(nm tokens.PackageName) string {
return stringNamePath(string(nm))