├── pkg ├── makefile │ ├── check.go │ ├── test.go │ ├── build.go │ ├── clean.go │ ├── scanner.go │ ├── run.go │ └── makefile.go └── exec │ ├── exec.go │ └── prefixwriter.go ├── main.mk ├── go.mod ├── cmd └── makeup │ ├── makeup.mk │ ├── main.go │ ├── commands │ ├── build.go │ ├── clean.go │ ├── test.go │ ├── root.go │ └── add.go │ └── cli │ └── cli.go ├── testapp ├── testapp.mk └── main.go ├── testapp2 ├── testapp2.mk └── main.go ├── .gitignore ├── go.sum ├── .goreleaser.yml ├── .github └── workflows │ ├── sanity.yml │ └── release.yml ├── README.md ├── USAGE.md └── LICENSE /pkg/makefile/check.go: -------------------------------------------------------------------------------- 1 | package makefile 2 | 3 | // Check is a version check 4 | type Check struct { 5 | Cmd string 6 | Equals string 7 | } 8 | -------------------------------------------------------------------------------- /main.mk: -------------------------------------------------------------------------------- 1 | # check go version 2 | # equal 1.21 3 | 4 | include ./testapp/testapp.mk 5 | include ./testapp2/testapp2.mk 6 | include ./cmd/makeup/makeup.mk -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cohix/makeup 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/pkg/errors v0.9.1 7 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c 8 | ) 9 | -------------------------------------------------------------------------------- /cmd/makeup/makeup.mk: -------------------------------------------------------------------------------- 1 | 2 | build: 3 | go build -o ${BIN_DEST} 4 | 5 | run: 6 | ${BIN_DEST} 7 | 8 | test: 9 | go test -v ./... 10 | 11 | env: 12 | echo "CONFIG_KEY=some_config_val" 13 | 14 | clean: 15 | rm ${BIN_DEST} 16 | -------------------------------------------------------------------------------- /testapp/testapp.mk: -------------------------------------------------------------------------------- 1 | 2 | build: 3 | go build -o ${BIN_DEST} 4 | 5 | run: 6 | ${BIN_DEST} 7 | 8 | test: 9 | go test -v ./... 10 | 11 | env: 12 | echo "SOMETHING_IMPORTANT=important value" 13 | 14 | clean: 15 | rm ${BIN_DEST} -------------------------------------------------------------------------------- /testapp2/testapp2.mk: -------------------------------------------------------------------------------- 1 | 2 | 3 | build: 4 | go build -o ${BIN_DEST} 5 | 6 | run: 7 | ${BIN_DEST} 8 | 9 | test: 10 | go test -v ./... 11 | 12 | env: 13 | echo "SOMETHING_IMPORTANT=another important value" 14 | 15 | clean: 16 | rm ${BIN_DEST} -------------------------------------------------------------------------------- /testapp/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | ) 8 | 9 | func main() { 10 | for { 11 | fmt.Println("Something important:", os.Getenv("SOMETHING_IMPORTANT")) 12 | 13 | time.Sleep(time.Second * 5) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /testapp2/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | ) 8 | 9 | func main() { 10 | for { 11 | fmt.Println("Another important thing:", os.Getenv("SOMETHING_IMPORTANT")) 12 | 13 | time.Sleep(time.Second * 3) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | .bin/ 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 2 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 3 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= 4 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 5 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | builds: 2 | - goos: 3 | - linux 4 | - darwin 5 | goarch: 6 | - amd64 7 | - arm64 8 | env: 9 | - CGO_ENABLED=0 10 | ldflags: 11 | - -extldflags=-static 12 | 13 | changelog: 14 | skip: true 15 | 16 | checksum: 17 | name_template: checksums.txt 18 | 19 | archives: 20 | - name_template: makeup-v{{ .Version }}-{{ .Os }}-{{ .Arch }} -------------------------------------------------------------------------------- /cmd/makeup/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log/slog" 5 | "os" 6 | 7 | "github.com/cohix/makeup/cmd/makeup/cli" 8 | "github.com/cohix/makeup/cmd/makeup/commands" 9 | ) 10 | 11 | func main() { 12 | cli.Setup( 13 | commands.Root, 14 | map[string]cli.Command{ 15 | "add": commands.Add, 16 | "build": commands.Build, 17 | "test": commands.Test, 18 | "clean": commands.Clean, 19 | }, 20 | ) 21 | 22 | if err := cli.Run(); err != nil { 23 | slog.Error(err.Error()) 24 | os.Exit(1) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /cmd/makeup/commands/build.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "github.com/cohix/makeup/pkg/makefile" 5 | "github.com/pkg/errors" 6 | ) 7 | 8 | // Build builds every component of the project 9 | func Build(args []string) error { 10 | mainmk, err := makefile.Parse("./main.mk") 11 | if err != nil { 12 | return errors.Wrap(err, "failed to Parse main.mk") 13 | } 14 | 15 | if err := mainmk.TestChecks(); err != nil { 16 | return errors.Wrap(err, "failed to TestChecks") 17 | } 18 | 19 | if err := mainmk.BuildAll(); err != nil { 20 | return errors.Wrap(err, "failed to BuildAll") 21 | } 22 | 23 | return nil 24 | } 25 | -------------------------------------------------------------------------------- /cmd/makeup/commands/clean.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "github.com/cohix/makeup/pkg/makefile" 5 | "github.com/pkg/errors" 6 | ) 7 | 8 | // Clean runs clean on every component of the project 9 | func Clean(args []string) error { 10 | mainmk, err := makefile.Parse("./main.mk") 11 | if err != nil { 12 | return errors.Wrap(err, "failed to Parse main.mk") 13 | } 14 | 15 | if err := mainmk.TestChecks(); err != nil { 16 | return errors.Wrap(err, "failed to TestChecks") 17 | } 18 | 19 | if err := mainmk.CleanAll(); err != nil { 20 | return errors.Wrap(err, "failed to CleanAll") 21 | } 22 | 23 | return nil 24 | } 25 | -------------------------------------------------------------------------------- /cmd/makeup/commands/test.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "github.com/cohix/makeup/pkg/makefile" 5 | "github.com/pkg/errors" 6 | ) 7 | 8 | // Test runs a test on every component of the project 9 | func Test(args []string) error { 10 | mainmk, err := makefile.Parse("./main.mk") 11 | if err != nil { 12 | return errors.Wrap(err, "failed to Parse main.mk") 13 | } 14 | 15 | if err := mainmk.TestChecks(); err != nil { 16 | return errors.Wrap(err, "failed to TestChecks") 17 | } 18 | 19 | if err := mainmk.TestAll(); err != nil { 20 | return errors.Wrap(err, "failed to TestAll") 21 | } 22 | 23 | return nil 24 | } 25 | -------------------------------------------------------------------------------- /cmd/makeup/commands/root.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "github.com/cohix/makeup/pkg/makefile" 5 | "github.com/pkg/errors" 6 | ) 7 | 8 | // Root is the root command 9 | func Root(args []string) error { 10 | mainmk, err := makefile.Parse("./main.mk") 11 | if err != nil { 12 | return errors.Wrap(err, "failed to Parse main.mk") 13 | } 14 | 15 | if err := mainmk.TestChecks(); err != nil { 16 | return errors.Wrap(err, "failed to TestChecks") 17 | } 18 | 19 | if err := mainmk.BuildAll(); err != nil { 20 | return errors.Wrap(err, "failed to BuildAll") 21 | } 22 | 23 | if err := mainmk.RunAll(); err != nil { 24 | return errors.Wrap(err, "failed to RunAll") 25 | } 26 | 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/sanity.yml: -------------------------------------------------------------------------------- 1 | name: Sanity 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | 11 | build: 12 | name: Test 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | - uses: actions/setup-go@v3 18 | with: 19 | go-version: '1.18' 20 | 21 | - name: Cache Go mods 22 | uses: actions/cache@v2 23 | with: 24 | path: | 25 | ~/.cache/go-build 26 | ~/go/pkg/mod 27 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 28 | restore-keys: | 29 | ${{ runner.os }}-go- 30 | 31 | - name: Get dependencies 32 | run: | 33 | go get -v -t -d ./... 34 | 35 | - name: Build 36 | run: | 37 | go install 38 | -------------------------------------------------------------------------------- /cmd/makeup/cli/cli.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | type Command func([]string) error 9 | 10 | var root Command 11 | var commands = map[string]Command{} 12 | 13 | // Setup sets up the CLI with a root command and subcommands 14 | func Setup(rootCmd Command, cmds map[string]Command) { 15 | root = rootCmd 16 | commands = cmds 17 | } 18 | 19 | // Run runs the CLI with super barebones arg parsing 20 | func Run() error { 21 | args := os.Args[1:] 22 | var cmd Command 23 | var ok bool 24 | 25 | switch len(os.Args) { 26 | case 1: 27 | cmd = root 28 | default: 29 | cmdName := os.Args[1] 30 | cmd, ok = commands[cmdName] 31 | if !ok { 32 | return fmt.Errorf("not a valid command: %s", cmdName) 33 | } 34 | 35 | // trim off the command name 36 | args = args[1:] 37 | } 38 | 39 | if err := cmd(args); err != nil { 40 | return err 41 | } 42 | 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: actions/setup-go@v3 15 | with: 16 | go-version: '1.18' 17 | 18 | - name: Cache Go mods 19 | uses: actions/cache@v2 20 | with: 21 | path: | 22 | ~/.cache/go-build 23 | ~/go/pkg/mod 24 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 25 | restore-keys: | 26 | ${{ runner.os }}-go- 27 | 28 | - name: Get dependencies 29 | run: | 30 | go get -v -t -d ./... 31 | 32 | - name: Run GoReleaser 33 | uses: goreleaser/goreleaser-action@v2 34 | with: 35 | version: latest 36 | args: release --rm-dist 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | -------------------------------------------------------------------------------- /pkg/makefile/test.go: -------------------------------------------------------------------------------- 1 | package makefile 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/cohix/makeup/pkg/exec" 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | // TestAll sequentially runs each of the project components' test targets 14 | func (m *Makefile) TestAll() error { 15 | cwd, err := os.Getwd() 16 | if err != nil { 17 | return errors.Wrap(err, "failed to Getwd") 18 | } 19 | 20 | binBase := filepath.Join(cwd, ".bin") 21 | 22 | for _, incl := range m.Includes { 23 | componentDir := filepath.Dir(incl.Path) 24 | componentMakefile := filepath.Base(incl.Path) 25 | componentName := strings.TrimSuffix(componentMakefile, ".mk") 26 | 27 | fmt.Println("testing:", componentName) 28 | 29 | binDest := filepath.Join(binBase, componentName) 30 | 31 | env := []string{ 32 | fmt.Sprintf("BIN_DEST=%s", binDest), 33 | } 34 | 35 | if _, err := exec.RunInDir(fmt.Sprintf("make -s -f %s test", componentMakefile), componentDir, nil, env...); err != nil { 36 | return errors.Wrapf(err, "failed to test %s", componentDir) 37 | } 38 | 39 | fmt.Println("test complete:", componentName) 40 | } 41 | 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /pkg/makefile/build.go: -------------------------------------------------------------------------------- 1 | package makefile 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/cohix/makeup/pkg/exec" 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | // BuildAll sequentially runs each of the project components' build targets 14 | func (m *Makefile) BuildAll() error { 15 | cwd, err := os.Getwd() 16 | if err != nil { 17 | return errors.Wrap(err, "failed to Getwd") 18 | } 19 | 20 | binBase := filepath.Join(cwd, ".bin") 21 | 22 | for _, incl := range m.Includes { 23 | componentDir := filepath.Dir(incl.Path) 24 | componentMakefile := filepath.Base(incl.Path) 25 | componentName := strings.TrimSuffix(componentMakefile, ".mk") 26 | 27 | fmt.Println("building:", componentName) 28 | 29 | binDest := filepath.Join(binBase, componentName) 30 | 31 | env := []string{ 32 | fmt.Sprintf("BIN_DEST=%s", binDest), 33 | } 34 | 35 | if _, err := exec.RunInDir(fmt.Sprintf("make -s -f %s build", componentMakefile), componentDir, nil, env...); err != nil { 36 | return errors.Wrapf(err, "failed to build %s", componentDir) 37 | } 38 | 39 | fmt.Println("build complete:", componentName) 40 | } 41 | 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /pkg/makefile/clean.go: -------------------------------------------------------------------------------- 1 | package makefile 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/cohix/makeup/pkg/exec" 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | // CleanAll sequentially runs each of the project components' clean targets 14 | func (m *Makefile) CleanAll() error { 15 | cwd, err := os.Getwd() 16 | if err != nil { 17 | return errors.Wrap(err, "failed to Getwd") 18 | } 19 | 20 | binBase := filepath.Join(cwd, ".bin") 21 | 22 | for _, incl := range m.Includes { 23 | componentDir := filepath.Dir(incl.Path) 24 | componentMakefile := filepath.Base(incl.Path) 25 | componentName := strings.TrimSuffix(componentMakefile, ".mk") 26 | 27 | fmt.Println("cleaning:", componentName) 28 | 29 | binDest := filepath.Join(binBase, componentName) 30 | 31 | env := []string{ 32 | fmt.Sprintf("BIN_DEST=%s", binDest), 33 | } 34 | 35 | if _, err := exec.RunInDir(fmt.Sprintf("make -s -f %s clean", componentMakefile), componentDir, nil, env...); err != nil { 36 | return errors.Wrapf(err, "failed to clean %s", componentDir) 37 | } 38 | 39 | fmt.Println("clean complete:", componentName) 40 | } 41 | 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /pkg/makefile/scanner.go: -------------------------------------------------------------------------------- 1 | package makefile 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "strings" 7 | "text/scanner" 8 | 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | type makeScanner struct { 13 | scn scanner.Scanner 14 | } 15 | 16 | func newScanner(rd io.Reader) *makeScanner { 17 | scn := scanner.Scanner{} 18 | scn.Init(rd) 19 | 20 | m := &makeScanner{ 21 | scn: scn, 22 | } 23 | 24 | return m 25 | } 26 | 27 | // readLine reads the next line of the file 28 | func (m *makeScanner) readLine() (string, error) { 29 | var err error 30 | 31 | m.scn.Error = func(_ *scanner.Scanner, msg string) { 32 | if msg != "" { 33 | err = errors.New(msg) 34 | } 35 | } 36 | 37 | buf := bytes.Buffer{} 38 | 39 | eof := false 40 | 41 | for { 42 | next := m.scn.Next() 43 | if next == scanner.EOF { 44 | eof = true 45 | break 46 | } 47 | 48 | stringNext := string(next) 49 | 50 | if err != nil { 51 | return "", errors.Wrap(err, "failed to scn.Next") 52 | } 53 | 54 | if stringNext == "\n" { 55 | break 56 | } 57 | 58 | if _, err := buf.Write([]byte(stringNext)); err != nil { 59 | return "", errors.Wrap(err, "failed to buf.Write") 60 | } 61 | } 62 | 63 | out := string(buf.Bytes()) 64 | 65 | // skip empty lines, recursively 66 | if !eof && strings.TrimSpace(out) == "" { 67 | return m.readLine() 68 | } 69 | 70 | return out, nil 71 | } 72 | -------------------------------------------------------------------------------- /pkg/exec/exec.go: -------------------------------------------------------------------------------- 1 | package exec 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "os" 7 | "os/exec" 8 | 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | // Run runs a command, outputting to terminal and returning the full output and/or error. 13 | func Run(cmd string, out io.Writer, env ...string) (string, error) { 14 | return run(cmd, "", false, out, env...) 15 | } 16 | 17 | // RunInDir runs a command in the specified directory and returns the full output or error. 18 | func RunInDir(cmd, dir string, out io.Writer, env ...string) (string, error) { 19 | return run(cmd, dir, false, out, env...) 20 | } 21 | 22 | // RunSilent runs a command without printing to stdout and returns the full output or error. 23 | func RunSilent(cmd string, dir string, env ...string) (string, error) { 24 | return run(cmd, dir, true, nil, env...) 25 | } 26 | 27 | func run(cmd, dir string, silent bool, out io.Writer, env ...string) (string, error) { 28 | // you can uncomment this below if you want to see exactly the commands being run 29 | // fmt.Println("▶️", cmd). 30 | 31 | command := exec.Command("sh", "-c", cmd) 32 | 33 | command.Dir = dir 34 | command.Env = append(os.Environ(), env...) 35 | 36 | var outBuf bytes.Buffer 37 | 38 | if silent { 39 | command.Stdout = &outBuf 40 | command.Stderr = &outBuf 41 | } else if out != nil { 42 | command.Stdout = io.MultiWriter(out, &outBuf) 43 | command.Stderr = io.MultiWriter(out, &outBuf) 44 | } else { 45 | command.Stdout = io.MultiWriter(os.Stdout, &outBuf) 46 | command.Stderr = io.MultiWriter(os.Stderr, &outBuf) 47 | } 48 | 49 | runErr := command.Run() 50 | 51 | outStr := outBuf.String() 52 | 53 | if runErr != nil { 54 | return outStr, errors.Wrap(runErr, "failed to Run command") 55 | } 56 | 57 | return outStr, nil 58 | } 59 | -------------------------------------------------------------------------------- /pkg/exec/prefixwriter.go: -------------------------------------------------------------------------------- 1 | package exec 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "strings" 8 | "sync" 9 | ) 10 | 11 | // PrefixWriter writes each line written into it to `out` prefixed with `prefix | ` 12 | type PrefixWriter struct { 13 | prefix string 14 | out io.Writer 15 | 16 | lock sync.Mutex 17 | buf []byte 18 | } 19 | 20 | // NewPrefixWriter creates a new PrefixWriter 21 | func NewPrefixWriter(prefix string, out io.Writer) *PrefixWriter { 22 | p := &PrefixWriter{ 23 | prefix: prefix, 24 | out: out, 25 | lock: sync.Mutex{}, 26 | } 27 | 28 | return p 29 | } 30 | 31 | // Write takes input bytes and seperates it into lines, writing each to `out` 32 | func (p *PrefixWriter) Write(in []byte) (int, error) { 33 | p.lock.Lock() 34 | defer p.lock.Unlock() 35 | 36 | inCopy := make([]byte, len(in)) 37 | copy(inCopy, in) 38 | 39 | if len(p.buf) == 0 { 40 | p.buf = inCopy 41 | } else { 42 | p.buf = append(p.buf, inCopy...) 43 | } 44 | 45 | if !strings.Contains(string(p.buf), "\n") { 46 | fmt.Println(string(p.buf)) 47 | return len(in), nil 48 | } else if len(p.buf) == 0 { 49 | return len(in), nil 50 | } 51 | 52 | fullLine := false 53 | if p.buf[len(p.buf)-1] == []byte("\n")[0] { 54 | fullLine = true 55 | } 56 | 57 | lines := bytes.Split(p.buf, []byte("\n")) 58 | 59 | if fullLine { 60 | p.buf = lines[len(lines)-1] 61 | lines = lines[:len(lines)-1] 62 | } 63 | 64 | for _, l := range lines { 65 | spaces := strings.Repeat(" ", 14-len(p.prefix)) 66 | prefixVal := fmt.Sprintf("%s%s| ", p.prefix, spaces) 67 | 68 | prefixedLine := append([]byte(prefixVal), l...) 69 | prefixedLine = append(prefixedLine, []byte("\n")...) 70 | 71 | p.out.Write(prefixedLine) 72 | } 73 | 74 | return len(in), nil 75 | } 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 💅🏽 Makeup 2 | 3 | > A local development tool to replace Docker Compose, based on Make. 4 | 5 | Makeup uses Makefiles to create a faster development workflow for microservices (when compared to Docker and docker-compose). It uses locally installed tools and a version-checking mechanism to get reasonably repeatable builds, and configures + runs things (like multiple microservices) in parallel, a la docker-compose. 6 | 7 | The `makeup` command builds all of the components of your application and runs them together without needing containers. 8 | 9 | To use `makeup`, you'll create: 10 | - The `main.mk` file (which includes tool version checks). 11 | - Individual component `.mk` files (with `build`, `run`, `test`, `env`, and `clean` targets). 12 | - Optional autogenerated `Makefile`, which allows anyone who isn't using `makeup` to build and run your project just as easily (by running `make up`). 13 | 14 | The `makeup` tool is required to get started (and to get the best experience), but not required for anyone else to use your project (which might be the best part). 15 | 16 | ✨ Check out the full [usage instructions](./USAGE.md) ✨ 17 | 18 | ### Commands 19 | 20 | Implemented: 21 | - `makeup`: builds each component sequentially, and then runs your entire project 22 | - `makeup test` : tests each component sequentially 23 | - `makeup clean` : cleans each of the components in the project 24 | 25 | In progress: 26 | - `makeup generate` : generates the main `Makefile` for anyone to use. 27 | 28 | Here's an example (from this repo!): 29 | 30 | Screen Shot 2022-03-28 at 6 02 12 PM 31 | 32 | That's it for now. 33 | 34 | Copyright Connor Hicks and external contributors, 2023. Apache-2.0 licensed, see LICENSE file. -------------------------------------------------------------------------------- /USAGE.md: -------------------------------------------------------------------------------- 1 | ## Using Makeup 2 | 3 | To use Makeup, you need two things; a `main.mk` in the root of your project, and a `.mk` in the subdirectory for each 'component' of your application. A component is anything you deem needing a build, test, run, clean loop in your workflow. 4 | 5 | To start, create `main.mk`: 6 | ```makefile 7 | # check go version 8 | # equal 1.18 9 | 10 | include ./testapp/testapp.mk 11 | include ./testapp2/testapp2.mk 12 | ``` 13 | 14 | The first two lines are a 'check', which ensure the correct versions of tools are being used. This can ensure that you and your team are all using the same version of a dependency, for example. You can have as many checks as you want, but they must be sequential lines that start with `# check` and `# equal`. 15 | 16 | > The equality is actually a 'contains' check, so even though `go version` outputs something like `go version go1.18 darwin/arm64`, since it contains `1.18`, it passes the check. 17 | 18 | The `include` statements are how you add components to the project. Each `.mk` file included in `main.mk` must have the following targets (even if they're empty): `build`, `run`, `test`, `clean`, `env`. For example: 19 | ```makefile 20 | build: 21 | go build -o ${BIN_DEST} 22 | 23 | run: 24 | ${BIN_DEST} 25 | 26 | test: 27 | go test -v ./... 28 | 29 | env: 30 | echo "SOMETHING_IMPORTANT=important value" 31 | 32 | clean: 33 | rm ${BIN_DEST} 34 | ``` 35 | 36 | Makeup uses these standard targets to control the lifecycle of your environment. 37 | 38 | You can run `makeup` to build each component and start them all, together. 39 | 40 | The output of the `env` target will be used to set environment variables when running each component. Using the `KEY=VALUE` syntax, you can use things like `echo` or `cat values.env` to load anything you need into each component's environment. 41 | 42 | Other commands include `makeup test` and `makeup clean` which run the `test` and `clean` targets on each of your components, sequentially. 43 | 44 | ## Generate Makefile 45 | Coming soon is the ability to run `makeup generate`. This will generate a `Makefile` that will simulate the workflow of makeup so that anyone can take advantage of these abilities, even if they don't have makeup installed. They'll just be able to run `make up` 😉 -------------------------------------------------------------------------------- /cmd/makeup/commands/add.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | const tmpl = ` 13 | build: 14 | go build -o ${BIN_DEST} 15 | 16 | run: 17 | ${BIN_DEST} 18 | 19 | test: 20 | go test -v ./... 21 | 22 | env: 23 | echo "CONFIG_KEY=some_config_val" 24 | 25 | clean: 26 | rm ${BIN_DEST} 27 | ` 28 | 29 | func Add(args []string) error { 30 | if len(args) < 1 { 31 | return errors.New("missing arg: component name") 32 | } 33 | 34 | componentArg := strings.ToLower(args[0]) 35 | componentArgParts := strings.Split(componentArg, fmt.Sprintf("%c", filepath.Separator)) 36 | componentName := componentArgParts[len(componentArgParts)-1] 37 | 38 | wd, err := os.Getwd() 39 | if err != nil { 40 | return errors.Wrap(err, "failed to Getwd") 41 | } 42 | 43 | componentDir := filepath.Join(wd, componentArg) 44 | 45 | _, err = os.Stat(componentDir) 46 | if err != nil { 47 | if errors.Is(err, os.ErrNotExist) { 48 | if mkErr := os.MkdirAll(componentDir, os.ModePerm); mkErr != nil { 49 | return errors.Wrapf(err, "failed to create component dir %s", componentDir) 50 | } 51 | } else { 52 | return errors.Wrapf(err, "failed to os.Stat %s", componentDir) 53 | } 54 | } 55 | 56 | filename := fmt.Sprintf("%s.mk", componentName) 57 | componentMkFilepath := filepath.Join(componentDir, filename) 58 | 59 | if err := os.WriteFile(componentMkFilepath, []byte(tmpl), os.ModePerm); err != nil { 60 | return errors.Wrapf(err, "failed to os.WriteFile %s", componentMkFilepath) 61 | } 62 | 63 | relativeComponentMkFilepath, err := filepath.Rel(wd, componentMkFilepath) 64 | if err != nil { 65 | return errors.Wrap(err, "failed to filepath.Rel") 66 | } 67 | 68 | includeStatement := fmt.Sprintf("\ninclude ./%s", relativeComponentMkFilepath) 69 | 70 | mainMkFilepath := filepath.Join(wd, "main.mk") 71 | 72 | _, err = os.Stat(mainMkFilepath) 73 | if err != nil { 74 | if errors.Is(err, os.ErrNotExist) { 75 | if err := os.WriteFile(mainMkFilepath, []byte(includeStatement), os.ModePerm); err != nil { 76 | return errors.Wrapf(err, "failed to os.WriteFile (new) %s", mainMkFilepath) 77 | } 78 | } else { 79 | return errors.Wrapf(err, "failed to os.Stat %s", mainMkFilepath) 80 | } 81 | } else { 82 | mkFile, err := os.OpenFile(mainMkFilepath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, os.ModePerm) 83 | if err != nil { 84 | return errors.Wrapf(err, "failed to os.OpenFile %s", mainMkFilepath) 85 | } 86 | 87 | defer mkFile.Close() 88 | 89 | if _, err = mkFile.WriteString(includeStatement); err != nil { 90 | return errors.Wrapf(err, "failed to WriteString %s", mainMkFilepath) 91 | } 92 | } 93 | 94 | fmt.Println("component created:", componentMkFilepath) 95 | 96 | return nil 97 | } 98 | -------------------------------------------------------------------------------- /pkg/makefile/run.go: -------------------------------------------------------------------------------- 1 | package makefile 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | "golang.org/x/sync/errgroup" 11 | 12 | "github.com/cohix/makeup/pkg/exec" 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | // RunAll runs all of the project components 17 | func (m *Makefile) RunAll() error { 18 | cwd, err := os.Getwd() 19 | if err != nil { 20 | return errors.Wrap(err, "failed to Getwd") 21 | } 22 | 23 | binBase := filepath.Join(cwd, ".bin") 24 | 25 | errGroup, _ := errgroup.WithContext(context.Background()) 26 | 27 | for _, incl := range m.Includes { 28 | componentDir := filepath.Dir(incl.Path) 29 | componentMakefile := filepath.Base(incl.Path) 30 | componentName := strings.TrimSuffix(componentMakefile, ".mk") 31 | 32 | fmt.Println("running:", componentName) 33 | 34 | binDest := filepath.Join(binBase, componentName) 35 | 36 | componentEnv, err := m.envForMkPath(incl.Path) 37 | if err != nil { 38 | return errors.Wrapf(err, "failed to envForMkPath %s", incl.Path) 39 | } 40 | 41 | // grab the 'env' target output and add some makeup-specific things 42 | env := append( 43 | strings.Split(componentEnv, "\n"), 44 | []string{ 45 | fmt.Sprintf("BIN_DEST=%s", binDest), 46 | }..., 47 | ) 48 | 49 | errGroup.Go(func() error { 50 | writer := exec.NewPrefixWriter(componentName, os.Stdout) 51 | 52 | if _, err := exec.RunInDir(fmt.Sprintf("make -s -f %s run", componentMakefile), componentDir, writer, env...); err != nil { 53 | return errors.Wrapf(err, "failed to run %s", componentDir) 54 | } 55 | 56 | return nil 57 | }) 58 | } 59 | 60 | return errGroup.Wait() 61 | } 62 | 63 | func (m *Makefile) envForMkPath(mkPath string) (string, error) { 64 | componentDir := filepath.Dir(mkPath) 65 | componentMakefile := filepath.Base(mkPath) 66 | componentName := strings.TrimSuffix(componentMakefile, ".mk") 67 | 68 | var out string 69 | var err error 70 | 71 | if m.ContainsOverride(componentName, "env") { 72 | overrideTarget := fmt.Sprintf("%s/%s", componentName, "env") 73 | 74 | out, err = exec.RunSilent(fmt.Sprintf("make -s -f %s %s", m.FullPath, overrideTarget), "") 75 | if err != nil { 76 | return "", errors.Wrapf(err, "failed to get override env %s", componentDir) 77 | } 78 | } else { 79 | out, err = exec.RunSilent(fmt.Sprintf("make -s -f %s env", componentMakefile), componentDir) 80 | if err != nil { 81 | return "", errors.Wrapf(err, "failed to get env %s", componentDir) 82 | } 83 | } 84 | 85 | envLines := []string{} 86 | 87 | // remove any lines that don't look like an env statement, i.e. KEY=VALUE 88 | outLines := strings.Split(out, "\n") 89 | for _, l := range outLines { 90 | if strings.Contains(l, "=") { 91 | envLines = append(envLines, l) 92 | } 93 | } 94 | 95 | return strings.Join(envLines, "\n"), nil 96 | } 97 | -------------------------------------------------------------------------------- /pkg/makefile/makefile.go: -------------------------------------------------------------------------------- 1 | package makefile 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/pkg/errors" 10 | 11 | "github.com/cohix/makeup/pkg/exec" 12 | ) 13 | 14 | const ( 15 | includePrefix = "include " 16 | checkPrefix = "# check " 17 | equalPrefix = "# equal " 18 | externPrefix = "# extern " 19 | rootPrefix = "# root " 20 | overrideLine = "# override" 21 | ) 22 | 23 | // Makefile is a lightly-parsed Makefile 24 | type Makefile struct { 25 | Checks []Check 26 | Includes []include 27 | Overrides []override 28 | 29 | FullPath string 30 | } 31 | 32 | // include represents an `include` statement in a Makefile, plus optional `extern` modifier 33 | type include struct { 34 | Path string 35 | Extern string 36 | } 37 | 38 | // override represents an overridden target for a component 39 | type override struct { 40 | Component string 41 | Target string 42 | } 43 | 44 | // Parse reads and parses the Makefile at the given path 45 | func Parse(path string) (*Makefile, error) { 46 | file, err := os.Open(path) 47 | if err != nil { 48 | return nil, errors.Wrapf(err, "failed to Open %s", path) 49 | } 50 | 51 | defer file.Close() 52 | 53 | mk, err := parse(file) 54 | if err != nil { 55 | return nil, errors.Wrapf(err, "failed to parse %s", path) 56 | } 57 | 58 | if err := mk.ensureIncludes(); err != nil { 59 | return nil, errors.Wrap(err, "failed to ensureIncludes") 60 | } 61 | 62 | fullPath, err := filepath.Abs(path) 63 | if err != nil { 64 | return nil, errors.Wrap(err, "failed to filepath.Abs") 65 | } 66 | 67 | mk.FullPath = fullPath 68 | 69 | return mk, nil 70 | } 71 | 72 | // TestChecks runs each defined Check and returns an error if any fail 73 | func (m *Makefile) TestChecks() error { 74 | for _, c := range m.Checks { 75 | out, err := exec.RunSilent(c.Cmd, "") 76 | if err != nil { 77 | return errors.Wrapf(err, "failed to RunSilent %s", c.Cmd) 78 | } 79 | 80 | if !strings.Contains(out, c.Equals) { 81 | return fmt.Errorf("failed check: %s is not %s, got %s", c.Cmd, c.Equals, out) 82 | } 83 | } 84 | 85 | return nil 86 | } 87 | 88 | // ContainsOverride returns true if the main.mk contains an overridden target for the given component 89 | func (m *Makefile) ContainsOverride(component, target string) bool { 90 | for _, o := range m.Overrides { 91 | if o.Component == component && o.Target == target { 92 | return true 93 | } 94 | } 95 | 96 | return false 97 | } 98 | 99 | func parse(file *os.File) (*Makefile, error) { 100 | mk := &Makefile{ 101 | Checks: []Check{}, 102 | Includes: []include{}, 103 | } 104 | 105 | scn := newScanner(file) 106 | 107 | for { 108 | line, err := scn.readLine() 109 | if err != nil { 110 | return nil, errors.Wrap(err, "failed to readLine") 111 | } 112 | 113 | if len(line) == 0 { 114 | break 115 | } 116 | 117 | if strings.HasPrefix(line, checkPrefix) { 118 | check := Check{ 119 | Cmd: strings.TrimPrefix(line, checkPrefix), 120 | } 121 | 122 | nextLine, err := scn.readLine() 123 | if err != nil { 124 | return nil, errors.Wrap(err, "failed to readLine") 125 | } 126 | 127 | if !strings.HasPrefix(nextLine, equalPrefix) { 128 | return nil, fmt.Errorf("line following check is not an 'equal' value (got %s)", nextLine) 129 | } 130 | 131 | check.Equals = strings.TrimPrefix(nextLine, equalPrefix) 132 | 133 | mk.Checks = append(mk.Checks, check) 134 | } else if strings.HasPrefix(line, includePrefix) { 135 | includePath := strings.TrimPrefix(line, includePrefix) 136 | 137 | incl := include{ 138 | Path: includePath, 139 | } 140 | 141 | mk.Includes = append(mk.Includes, incl) 142 | } else if strings.HasPrefix(line, externPrefix) { 143 | externPath := strings.TrimPrefix(line, externPrefix) 144 | 145 | includeLine, err := scn.readLine() 146 | if err != nil { 147 | return nil, errors.Wrap(err, "failed to readLine") 148 | } 149 | 150 | if !strings.HasPrefix(includeLine, includePrefix) { 151 | return nil, fmt.Errorf("line following extern is not an 'include' statement (got %s)", includeLine) 152 | } 153 | 154 | includePath := strings.TrimPrefix(includeLine, includePrefix) 155 | 156 | incl := include{ 157 | Path: includePath, 158 | Extern: externPath, 159 | } 160 | 161 | mk.Includes = append(mk.Includes, incl) 162 | } else if line == overrideLine { 163 | targetLine, err := scn.readLine() 164 | if err != nil { 165 | return nil, errors.Wrap(err, "failed to readLine") 166 | } 167 | 168 | if !strings.Contains(targetLine, ":") { 169 | return nil, fmt.Errorf("line following override is not a target (got %s)", targetLine) 170 | } 171 | 172 | fullTarget := targetLine[:strings.Index(targetLine, ":")] 173 | targetParts := strings.Split(fullTarget, "/") 174 | if len(targetParts) != 2 { 175 | return nil, fmt.Errorf("override targed must have two /-seperated parts (got %d)", len(targetParts)) 176 | } 177 | 178 | component := targetParts[0] 179 | target := targetParts[1] 180 | 181 | ovr := override{ 182 | Component: component, 183 | Target: target, 184 | } 185 | 186 | mk.Overrides = append(mk.Overrides, ovr) 187 | } 188 | } 189 | 190 | return mk, nil 191 | } 192 | 193 | func (m *Makefile) ensureIncludes() error { 194 | for _, incl := range m.Includes { 195 | if _, err := os.Stat(incl.Path); err != nil { 196 | if errors.Is(err, os.ErrNotExist) { 197 | if incl.Extern != "" { 198 | return errors.Wrapf(err, "missing %s from extern %s", incl.Path, incl.Extern) 199 | } else { 200 | return errors.Wrap(err, "missing %s") 201 | } 202 | } 203 | 204 | return errors.Wrapf(err, "failed to Stat %s", incl.Path) 205 | } 206 | } 207 | 208 | return nil 209 | } 210 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------