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