Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 91 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,91 @@
# makego-rawan
# Makego

This repository provides a Go-based tool to execute Makefile targets with support for concurrency.

## Features

- Execute Makefile targets directly from Go.
- Concurrency support: independent stages can run in parallel.
- Ordered execution for dependent stages.
- Error handling through channels for safe concurrent execution.


## Installation

1. Clone this repository:

``` bash
git clone https://github.com/codescalersinternships/makego-rawan.git
cd makego-rawan
```

## Usage

### Code Example

```go
import makego "github.com/codescalersinternships/makego-rawan/pkg"

func main() {
err := makego.ExecuteMakefile(path, targets)
if err != nil {
panic(err)
}
}
```

### Running a Makefile Target

``` bash
go run cmd/main.go -f "testdata/makefile" target
```

### Example

Given the following Makefile:

``` makefile
hello: hello1.txt hello2.txt
cat hello1.txt
cat hello2.txt

hello1.txt:
echo "Hello 1, Make!" > hello1.txt

hello2.txt:
echo "Hello 2, Make!" > hello2.txt

clean:
rm -f hello1.txt
rm -f hello2.txt

```

Running:

``` bash
go run cmd/main.go -f "testdata/makefile" hello
```

Will produce:

echo "Hello 2, Make!" > hello2.txt
echo "Hello 1, Make!" > hello1.txt
cat hello1.txt
Hello 1, Make!
cat hello2.txt
Hello 2, Make!

### Flags

- `-f`: The makefile to be executed (must be provided)

### Arguments

- `<target>` : The target to be run (optional)
- default: first target in the makefile

## Concurrency

- Independent targets run in parallel
- Channels are used to handle errors
43 changes: 43 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package main

import (
"flag"
"fmt"
"strings"

makego "github.com/codescalersinternships/makego-rawan/pkg"
)

func PrintRules(rules []makego.Stage) {
for _, r := range rules {
if len(r.Dependencies) > 0 {
fmt.Printf("%s: %s\n", r.Target, strings.Join(r.Dependencies, " "))
} else {
fmt.Printf("%s:\n", r.Target)
}

for _, cmd := range r.Commands {
fmt.Printf("\t%s\n", cmd)
}

fmt.Println()
}
}
func main() {
file := flag.String("f", "Makefile", "Path to the makefile")
flag.Parse()

args := flag.Args()

var targets []string
targets = nil

if len(args) > 0 {
targets = args[0:]
}

err := makego.ExecuteMakefile(*file, targets)
if err != nil {
panic(err)
}
}
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/codescalersinternships/makego-rawan

go 1.25.0
49 changes: 49 additions & 0 deletions pkg/depsHandler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package makego

import "slices"

func buildGraph(stages []Stage) map[string][]string {
graph := make(map[string][]string)
for _, s := range stages {
graph[s.Target] = s.Dependencies
}
return graph
}

func dfs(node string, graph map[string][]string, visited map[string]bool, stack []string, orderedTargets *[]string) error {
if visited[node] {
return nil
}
if slices.Contains(stack, node) {
return ErrCircularDependency
}

stack = append(stack, node)

for _, dep := range graph[node] {
err := dfs(dep, graph, visited, stack, orderedTargets)
if err != nil {
return err
}
}
visited[node] = true
*orderedTargets = append(*orderedTargets, node)
return nil
}

func dependencyResolver(stages []Stage, root string) ([]string, error) {
graph := buildGraph(stages)
visited := make(map[string]bool)
stack := make([]string, 0)
orderedTargets := make([]string, 0)

for target := range graph {
if !visited[target] {
err := dfs(root, graph, visited, stack, &orderedTargets)
if err != nil {
return nil, err
}
}
}
return orderedTargets, nil
}
58 changes: 58 additions & 0 deletions pkg/depsHandler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package makego

import (
"reflect"
"testing"
)

func TestDependencyResolver(t *testing.T) {
testcases := []struct {
name string
stages []Stage
expectError error
expected []string
}{
{
name: "Simple linear dependencies",
stages: []Stage{
{Target: "A", Dependencies: []string{"B"}, Commands: []string{"echo A"}},
{Target: "B", Dependencies: []string{"C"}, Commands: []string{"echo B"}},
{Target: "C", Dependencies: []string{}, Commands: []string{"echo C"}},
},
expectError: nil,
expected: []string{"C", "B", "A"},
},
{
name: "Circular dependency",
stages: []Stage{
{Target: "A", Dependencies: []string{"B"}, Commands: []string{"echo A"}},
{Target: "B", Dependencies: []string{"A"}, Commands: []string{"echo B"}},
},
expectError: ErrCircularDependency,
expected: nil,
},
{
name: "Indirect Circular dependency",
stages: []Stage{
{Target: "A", Dependencies: []string{"B"}, Commands: []string{"echo A"}},
{Target: "B", Dependencies: []string{"C"}, Commands: []string{"echo C"}},
{Target: "C", Dependencies: []string{"D"}, Commands: []string{"echo D"}},
{Target: "D", Dependencies: []string{"A"}, Commands: []string{"echo A"}},
},
expectError: ErrCircularDependency,
expected: nil,
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
ordered, err := dependencyResolver(tc.stages, tc.stages[0].Target)
if err != tc.expectError {
t.Errorf("expected error: %v, got: %v", tc.expectError, err)
}

if !reflect.DeepEqual(ordered, tc.expected) {
t.Fatalf("expected order: %v, got: %v", tc.expected, ordered)
}
})
}
}
97 changes: 97 additions & 0 deletions pkg/executer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package makego

import (
"fmt"
"os"
"os/exec"
"sync"
)

func getStageByTarget(stages []Stage, target string) *Stage {
for i, s := range stages {
if s.Target == target {
return &stages[i]
}
}
return nil
}

func ExecuteMakefile(path string, targets []string) error {
makefile, err := parseMakefile(path)
if err != nil {
return err
}
defaultTarget := makefile.Stages[0].Target

if len(targets) == 0 {
targets = []string{defaultTarget}
}

for _, target := range targets {
ordered, err := dependencyResolver(makefile.Stages, target) // get the ordered list of targets to execute
if err != nil {
return err
}

done := make(map[string]bool)

for i := 0; i < len(ordered); {
var wg sync.WaitGroup // wait group to wait for all goroutines to finish
ch := make(chan error, len(ordered)) // channel to collect errors from goroutines
level := []string{}
for j := i; j < len(ordered); j++ {
stage := getStageByTarget(makefile.Stages, ordered[j])
if stage == nil {
continue
}
ready := true
for _, dep := range stage.Dependencies { // check if all dependencies of the current target are done
if !done[dep] {
ready = false
break
}
}

if ready {
level = append(level, ordered[j])
}

}
for _, t := range level {
wg.Add(1) // increment wait group counter (means wait for one more goroutine)
go func(target string) {
defer wg.Done()
stage := getStageByTarget(makefile.Stages, target)
err := runStage(stage)
if err != nil {
ch <- err
return
}
done[target] = true

}(t)
}
wg.Wait()
close(ch)
if len(ch) > 0 {
return <-ch
}
i += len(level)
}
}
return nil
}

func runStage(stage *Stage) error {
for _, cmd := range stage.Commands {
fmt.Print(cmd + "\n")
cmd := exec.Command("sh", "-c", cmd)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
return err
}
}
return nil
}
Loading