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:
parent
9c7f774fc6
commit
e75f06bb2b
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
vendor/
|
||||
|
7
Makefile
Normal file
7
Makefile
Normal 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
38
cmd/build.go
Normal 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
17
cmd/cmd.go
Normal 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
21
cmd/version.go
Normal 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
20
glide.lock
generated
Normal 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
10
glide.yaml
Normal 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
24
main.go
Normal 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
59
pkg/compiler/compiler.go
Normal 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
19
pkg/compiler/opts.go
Normal 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
85
pkg/compiler/parser.go
Normal 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
34
pkg/diag/diag.go
Normal 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
21
pkg/diag/pos.go
Normal 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
88
pkg/diag/sink.go
Normal 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
12
pkg/errors/compiler.go
Normal 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
22
pkg/errors/parser.go
Normal 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
73
pkg/workspace/paths.go
Normal 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
|
||||
}
|
Loading…
Reference in a new issue