Sketch a mu build command and its scaffolding

This adds a bunch of general scaffolding and the beginning of a `build` command.

The general engineering scaffolding includes:

* Glide for dependency management.
* A Makefile that runs govet and golint during builds.
* Google's Glog library for logging.
* Cobra for command line functionality.

The Mu-specific scaffolding includes some packages:

* mu/pkg/diag: A package for compiler-like diagnostics.  It's fairly barebones
  at the moment, however we can embellish this over time.
* mu/pkg/errors: A package containing Mu's predefined set of errors.
* mu/pkg/workspace: A package containing workspace-related convenience helpers.

in addition to a main entrypoint that simply wires up and invokes the CLI.  From
there, the mu/cmd package takes over, with the Cobra-defined CLI commands.

Finally, the mu/pkg/compiler package actually implements the compiler behavior.
Or, it will.  For now, it simply parses a JSON or YAML Mufile into the core
mu/pkg/api types, and prints out the result.
This commit is contained in:
joeduffy 2016-11-15 14:30:34 -05:00
parent 9c7f774fc6
commit e75f06bb2b
17 changed files with 552 additions and 0 deletions

2
.gitignore vendored Normal file
View file

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

7
Makefile Normal file
View file

@ -0,0 +1,7 @@
PROJECT=github.com/marapongo/mu
all:
go install ${PROJECT}
golint ${PROJECT}
go vet ${PROJECT}

38
cmd/build.go Normal file
View file

@ -0,0 +1,38 @@
// Copyright 2016 Marapongo, Inc. All rights reserved.
package cmd
import (
"github.com/marapongo/mu/pkg/compiler"
"github.com/spf13/cobra"
)
// defaultIn is where the Mu compiler looks for inputs by default.
const defaultInp = "."
// defaultOutput is where the Mu compiler places build artifacts by default.
const defaultOutp = ".mu"
func newBuildCmd() *cobra.Command {
var outp string
var cmd = &cobra.Command{
Use: "build [source]",
Short: "Compile a Mu Stack",
Run: func(cmd *cobra.Command, args []string) {
inp := defaultInp
if len(args) > 0 {
inp = args[0]
}
mup := compiler.NewCompiler(compiler.DefaultOpts())
mup.Build(inp, outp)
},
}
cmd.PersistentFlags().StringVar(
&outp, "out", defaultOutp,
"The directory in which to place build artifacts",
)
return cmd
}

17
cmd/cmd.go Normal file
View file

@ -0,0 +1,17 @@
// Copyright 2016 Marapongo, Inc. All rights reserved.
package cmd
import (
"github.com/spf13/cobra"
)
var Cmd = &cobra.Command{
Use: "mu",
Short: "Mu is a framework and toolset for reusable stacks of services",
}
func init() {
Cmd.AddCommand(newBuildCmd())
Cmd.AddCommand(newVersionCmd())
}

21
cmd/version.go Normal file
View file

@ -0,0 +1,21 @@
// Copyright 2016 Marapongo, Inc. All rights reserved.
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
const version = "0.0.1" // TODO: a real auto-incrementing version number.
func newVersionCmd() *cobra.Command {
return &cobra.Command{
Use: "version",
Short: "Print Mu's version number",
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("Mu version %v\n", version)
},
}
}

20
glide.lock generated Normal file
View file

@ -0,0 +1,20 @@
hash: 4c4bc0757150232b4473c5ac62d3c5b5e13c6e07ac2e580f901b95cdb6508498
updated: 2016-11-15T14:05:43.951339623-05:00
imports:
- name: github.com/ghodss/yaml
version: bea76d6a4713e18b7f5321a2b020738552def3ea
- name: github.com/golang/glog
version: 23def4e6c14b4da8ac2ed8007337bc5eb5007998
- name: github.com/golang/lint
version: 206c0f020eba0f7fbcfbc467a5eb808037df2ed6
subpackages:
- golint
- name: github.com/inconshreveable/mousetrap
version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75
- name: github.com/spf13/cobra
version: 6b74a60562f5c1c920299b8f02d153e16f4897fc
- name: github.com/spf13/pflag
version: 5ccb023bc27df288a957c5e994cd44fd19619465
- name: gopkg.in/yaml.v2
version: a5b47d31c556af34a302ce5d659e6fea44d90de0
devImports: []

10
glide.yaml Normal file
View file

@ -0,0 +1,10 @@
package: github.com/marapongo/mu
import:
- package: github.com/ghodss/yaml
- package: github.com/spf13/cobra
- package: github.com/spf13/pflag
- package: gopkg.in/yaml.v2
- package: github.com/golang/glog
- package: github.com/golang/lint
subpackages:
- golint

24
main.go Normal file
View file

@ -0,0 +1,24 @@
// Copyright 2016 Marapongo, Inc. All rights reserved.
package main
import (
"flag"
"fmt"
"os"
"github.com/golang/glog"
"github.com/marapongo/mu/cmd"
)
func main() {
// Ensure the glog library has been initialized, including calling flag.Parse beforehand.
flag.Parse()
glog.Info("Mu CLI is running")
if err := cmd.Cmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(-1)
}
}

59
pkg/compiler/compiler.go Normal file
View file

@ -0,0 +1,59 @@
// Copyright 2016 Marapongo, Inc. All rights reserved.
package compiler
import (
"fmt"
"github.com/golang/glog"
"github.com/marapongo/mu/pkg/diag"
"github.com/marapongo/mu/pkg/errors"
"github.com/marapongo/mu/pkg/workspace"
)
// Compiler provides an interface into the many phases of the Mu compilation process.
type Compiler interface {
// Diags fetches the diagnostics sink used by this compiler instance.
Diags() diag.Sink
// Build detects and compiles inputs from the given location, storing build artifacts in the given destination.
Build(inp string, outp string)
}
// compiler is the canonical implementation of the Mu compiler.
type compiler struct {
opts Options
}
// NewCompiler creates a new instance of the Mu compiler, with the given initialization settings.
func NewCompiler(opts Options) Compiler {
return &compiler{opts}
}
func (c *compiler) Diags() diag.Sink {
return c.opts.Diags
}
func (c *compiler) Build(inp string, outp string) {
glog.Infof("Building target directory '%v' to '%v'", inp, outp)
// First find the root of the current package based on the location of its Mufile.
mufile, err := workspace.DetectMufile(inp)
if err != nil {
c.Diags().Errorf(errors.MissingMufile, inp)
return
}
// To build the Mu package, first parse the input file.
p := NewParser(c)
stack := p.Parse(mufile)
// If any errors happened during parsing, we cannot proceed; exit now.
if c.Diags().Errors() > 0 {
return
}
fmt.Printf("PARSED: %v\n", stack)
}

19
pkg/compiler/opts.go Normal file
View file

@ -0,0 +1,19 @@
// Copyright 2016 Marapongo, Inc. All rights reserved.
package compiler
import (
"github.com/marapongo/mu/pkg/diag"
)
// Options contains all of the settings a user can use to control the compiler's behavior.
type Options struct {
Diags diag.Sink
}
// DefaultOpts returns the default set of compiler options.
func DefaultOpts() Options {
return Options{
Diags: diag.DefaultSink(),
}
}

85
pkg/compiler/parser.go Normal file
View file

@ -0,0 +1,85 @@
// Copyright 2016 Marapongo, Inc. All rights reserved.
package compiler
import (
"encoding/json"
"io/ioutil"
"path/filepath"
"github.com/ghodss/yaml"
"github.com/golang/glog"
"github.com/marapongo/mu/pkg/api"
"github.com/marapongo/mu/pkg/diag"
"github.com/marapongo/mu/pkg/errors"
)
type Parser interface {
// Diags fetches the diagnostics sink used by this parser.
Diags() diag.Sink
// Parse detects and parses input from the given path. If an error occurs, the return value will be nil. It is
// expected that errors are conveyed using the diag.Sink interface.
Parse(inp string) *api.Stack
}
func NewParser(c Compiler) Parser {
return &parser{c}
}
type parser struct {
c Compiler
}
func (p *parser) Diags() diag.Sink {
return p.c.Diags()
}
func (p *parser) Parse(mufile string) *api.Stack {
glog.Infof("Parsing Mufile '%v'", mufile)
// We support both JSON and YAML as a file format. Detect the file extension and deserialize the contents.
ext := filepath.Ext(mufile)
switch ext {
case "json":
return p.parseFromJSON(mufile)
case "yaml":
return p.parseFromYAML(mufile)
default:
p.Diags().Errorf(errors.IllegalMufileExt.WithFile(mufile), ext)
return nil
}
}
func (p *parser) parseFromJSON(mufile string) *api.Stack {
body, err := ioutil.ReadFile(mufile)
if err != nil {
p.Diags().Errorf(errors.CouldNotReadMufile.WithFile(mufile), err)
return nil
}
var stack api.Stack
if err := json.Unmarshal(body, &stack); err != nil {
p.Diags().Errorf(errors.IllegalMufileSyntax.WithFile(mufile), err)
// TODO: it would be great if we issued an error per issue found in the file with line/col numbers.
return nil
}
return &stack
}
func (p *parser) parseFromYAML(mufile string) *api.Stack {
body, err := ioutil.ReadFile(mufile)
if err != nil {
p.Diags().Errorf(errors.CouldNotReadMufile.WithFile(mufile), err)
return nil
}
var stack api.Stack
if err := yaml.Unmarshal(body, &stack); err != nil {
p.Diags().Errorf(errors.IllegalMufileSyntax.WithFile(mufile), err)
// TODO: it would be great if we issued an error per issue found in the file with line/col numbers.
return nil
}
return &stack
}

34
pkg/diag/diag.go Normal file
View file

@ -0,0 +1,34 @@
// Copyright 2016 Marapongo, Inc. All rights reserved.
package diag
// ID is a unique diagnostics identifier.
type ID int
// Diag is an instance of an error or warning generated by the compiler.
type Diag struct {
ID ID // a unique identifier for this diagnostic.
Message string // a human-friendly message for this diagnostic.
File string // the document in which this diagnostic occurred.
Filepos *PosRange // the document position at which this diagnostic occurred.
}
// WithFile adds a file to an existing diagnostic, retaining its ID and message.
func (diag *Diag) WithFile(file string) *Diag {
return &Diag{
ID: diag.ID,
Message: diag.Message,
File: file,
Filepos: &EmptyPosRange,
}
}
// WithFilepos adds a file and position to an existing diagnostic, retaining its ID and message.
func (diag *Diag) WithFilepos(file string, filepos *PosRange) *Diag {
return &Diag{
ID: diag.ID,
Message: diag.Message,
File: file,
Filepos: filepos,
}
}

21
pkg/diag/pos.go Normal file
View file

@ -0,0 +1,21 @@
// Copyright 2016 Marapongo, Inc. All rights reserved.
package diag
// Pos represents a position in a file.
type Pos struct {
Row int
Col int
}
// EmptyPos may be used when no position is needed.
var EmptyPos = Pos{0, 0}
// PosRange represents a position range in a file.
type PosRange struct {
Start Pos
End Pos
}
// EmptyPosRange may be used when no position range is needed.
var EmptyPosRange = PosRange{EmptyPos, EmptyPos}

88
pkg/diag/sink.go Normal file
View file

@ -0,0 +1,88 @@
// Copyright 2016 Marapongo, Inc. All rights reserved.
package diag
import (
"bytes"
"fmt"
"os"
"strconv"
)
// Sink facilitates pluggable diagnostics messages.
type Sink interface {
// Count fetches the total number of diagnostics issued (errors plus warnings).
Count() int
// Errors fetches the number of errors issued.
Errors() int
// Warnings fetches the number of warnings issued.
Warnings() int
// Error issues a new error diagnostic.
Errorf(diag *Diag, args ...interface{})
// Warning issues a new warning diagnostic.
Warningf(diag *Diag, args ...interface{})
}
// DefaultDiags returns a default sink that simply logs output to stderr/stdout.
func DefaultSink() Sink {
return &defaultDiags{}
}
// defaultDiags is the default sink which logs output to stderr/stdout.
type defaultDiags struct {
errors int
warnings int
}
func (d *defaultDiags) Count() int {
return d.errors + d.warnings
}
func (d *defaultDiags) Errors() int {
return d.errors
}
func (d *defaultDiags) Warnings() int {
return d.warnings
}
func (d *defaultDiags) Errorf(diag *Diag, args ...interface{}) {
fmt.Fprintln(os.Stdout, d.stringify(diag, "error", args...))
}
func (d *defaultDiags) Warningf(diag *Diag, args ...interface{}) {
fmt.Fprintln(os.Stdout, d.stringify(diag, "warning", args...))
}
// stringify stringifies a diagnostic in the usual way (e.g., "error: MU123: Mu.yaml:7:39: error goes here\n").
func (d *defaultDiags) stringify(diag *Diag, prefix string, args ...interface{}) string {
var buffer bytes.Buffer
buffer.WriteString(prefix)
buffer.WriteString(": ")
if diag.ID > 0 {
buffer.WriteString("MU")
buffer.WriteString(strconv.Itoa(int(diag.ID)))
buffer.WriteString(": ")
}
if diag.File != "" {
buffer.WriteString(diag.File)
if diag.Filepos != nil {
buffer.WriteRune(':')
buffer.WriteString(strconv.Itoa(diag.Filepos.Start.Row))
buffer.WriteRune(':')
buffer.WriteString(strconv.Itoa(diag.Filepos.Start.Col))
}
buffer.WriteString(": ")
}
buffer.WriteString(fmt.Sprintf(diag.Message, args...))
buffer.WriteRune('\n')
// TODO: support Clang-style caret diagnostics; e.g., see http://clang.llvm.org/diagnostics.html.
return buffer.String()
}

12
pkg/errors/compiler.go Normal file
View file

@ -0,0 +1,12 @@
// Copyright 2016 Marapongo, Inc. All rights reserved.
package errors
import (
"github.com/marapongo/mu/pkg/diag"
)
var MissingMufile = &diag.Diag{
ID: 100,
Message: "No Mufile was found in the given path or any of its parents (%v)",
}

22
pkg/errors/parser.go Normal file
View file

@ -0,0 +1,22 @@
// Copyright 2016 Marapongo, Inc. All rights reserved.
package errors
import (
"github.com/marapongo/mu/pkg/diag"
)
var IllegalMufileExt = &diag.Diag{
ID: 1500,
Message: "A file named `Mufile` was located, but '%v' isn't a valid file extension (must be .json or .yaml)",
}
var CouldNotReadMufile = &diag.Diag{
ID: 1501,
Message: "An IO error occurred while reading the Mufile: %v",
}
var IllegalMufileSyntax = &diag.Diag{
ID: 1502,
Message: "A syntax error was detected while parsing the Mufile: %v",
}

73
pkg/workspace/paths.go Normal file
View file

@ -0,0 +1,73 @@
// Copyright 2016 Marapongo, Inc. All rights reserved.
package workspace
import (
"errors"
"io/ioutil"
"os"
"path/filepath"
)
const mufileBase = "Mu"
var mufileExts = []string{"json", "yaml"}
// DetectMufile locates the closest Mufile from the given path, searching "upwards" in the directory hierarchy. If
// no Mufile is found, a non-nil error is returned.
func DetectMufile(from string) (string, error) {
abs, err := filepath.Abs(from)
if err != nil {
return "", err
}
// It's possible the target is already the file we seek; if so, return right away.
if IsMufile(abs) {
return abs, nil
}
curr := abs
for {
// If the target is a directory, enumerate its files, checking each to see if it's a Mufile.
files, err := ioutil.ReadDir(curr)
if err != nil {
return "", err
}
for _, file := range files {
path := filepath.Join(curr, file.Name())
if IsMufile(path) {
return path, nil
}
}
// If neither succeeded, keep looking in our parent directory.
curr = filepath.Dir(curr)
if os.IsPathSeparator(curr[len(curr)-1]) {
break
}
}
return "", errors.New("No Mufile found")
}
// IsMufile returns true if the path references what appears to be a valid Mufile.
func IsMufile(path string) bool {
info, err := os.Stat(path)
if err != nil {
return false
}
// Directories can't be Mufiles.
if info.IsDir() {
return false
}
// Check all supported extensions.
for _, ext := range mufileExts {
if info.Name() == mufileBase+"."+ext {
return true
}
}
return false
}