From 32d8a9d4444b01f856e58cca887b2dacbfa8e086 Mon Sep 17 00:00:00 2001 From: RawanMostafa08 Date: Sun, 14 Sep 2025 16:50:20 +0300 Subject: [PATCH 01/13] feat: parser initial version --- cmd/main.go | 31 +++++++++++++++++++++ go.mod | 3 ++ pkg/parser.go | 70 +++++++++++++++++++++++++++++++++++++++++++++++ testdata/makefile | 13 +++++++++ 4 files changed, 117 insertions(+) create mode 100644 cmd/main.go create mode 100644 go.mod create mode 100644 pkg/parser.go create mode 100644 testdata/makefile diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..6ae4ed3 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,31 @@ +package main + +import ( + "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() { + makefile, err := makego.ParseMakefile("testdata/makefile") + if err != nil { + panic(err) + } + PrintRules(makefile.Stages) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2a18726 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/codescalersinternships/makego-rawan + +go 1.25.0 diff --git a/pkg/parser.go b/pkg/parser.go new file mode 100644 index 0000000..2644765 --- /dev/null +++ b/pkg/parser.go @@ -0,0 +1,70 @@ +package makego + +import ( + "fmt" + "os" + "path/filepath" + "strings" +) + +type Stage struct { + Target string + Dependencies []string + Commands []string +} + +type Makefile struct { + Stages []Stage +} + +func readMakefile(path string) (string, error) { + filename := filepath.Base(path) + if strings.ToLower(filename) != "makefile" { + return "", fmt.Errorf("the specified file is not a Makefile") + } + + data, err := os.ReadFile(path) + if err != nil { + return "", err + } + + return string(data), nil +} + +func ParseMakefile(path string) (*Makefile, error) { + content, err := readMakefile(path) + if err != nil { + return nil, err + } + lines := strings.Split(content, "\n") + var stages []Stage + for i, line := range lines { + // line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + + if strings.Contains(line, ":") { // target line + parts := strings.Split(line, ":") + if len(parts) != 2 { + return nil, fmt.Errorf("syntax error on line %d: %s", i+1, line) + } + + target := strings.TrimSpace(parts[0]) + dependencies_str := strings.TrimSpace(parts[1]) + dependencies := strings.Split(dependencies_str, " ") + stages = append(stages, Stage{Target: target, Dependencies: dependencies}) + + } else if strings.HasPrefix(line, "\t") { // command line + if len(stages) == 0 { + return nil, fmt.Errorf("command without target on line %d: %s", i+1, line) + } + command := strings.TrimSpace(line) + currStage := &stages[len(stages)-1] + currStage.Commands = append(currStage.Commands, command) + } else { + return nil, fmt.Errorf("unrecognized line format on line %d: %s", i+1, line) + } + } + return &Makefile{Stages: stages}, nil +} diff --git a/testdata/makefile b/testdata/makefile new file mode 100644 index 0000000..5c01ab3 --- /dev/null +++ b/testdata/makefile @@ -0,0 +1,13 @@ +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 From 6b46bffb52af017c413aaa24ec8735bbc6a11f7c Mon Sep 17 00:00:00 2001 From: RawanMostafa08 Date: Mon, 15 Sep 2025 12:39:07 +0300 Subject: [PATCH 02/13] feat: dependency graph, cyclic deps detection --- cmd/main.go | 8 ++++-- pkg/parser.go | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 6ae4ed3..f5a5d02 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -23,9 +23,13 @@ func PrintRules(rules []makego.Stage) { } } func main() { - makefile, err := makego.ParseMakefile("testdata/makefile") + // makefile, err := makego.ParseMakefile("testdata/makefile") + // if err != nil { + // panic(err) + // } + // PrintRules(makefile.Stages) + err := makego.ExecuteMakefile("testdata/makefile") if err != nil { panic(err) } - PrintRules(makefile.Stages) } diff --git a/pkg/parser.go b/pkg/parser.go index 2644765..4e8103e 100644 --- a/pkg/parser.go +++ b/pkg/parser.go @@ -1,12 +1,16 @@ package makego import ( + "errors" "fmt" "os" "path/filepath" + "slices" "strings" ) +var ErrCircularDependency = errors.New("circular dependency detected") + type Stage struct { Target string Dependencies []string @@ -31,6 +35,52 @@ func readMakefile(path string) (string, error) { return string(data), nil } +func buildGraph(stages []Stage) map[string][]string { + graph := make(map[string][]string) + for _, s := range stages { + graph[s.Target] = s.Dependencies + } + return graph +} + +func dependencyResolver(stages []Stage) ([]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(target, graph, visited, stack, &orderedTargets) + if err != nil { + return nil, err + } + } + } + return orderedTargets, nil +} + +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 ParseMakefile(path string) (*Makefile, error) { content, err := readMakefile(path) if err != nil { @@ -52,7 +102,7 @@ func ParseMakefile(path string) (*Makefile, error) { target := strings.TrimSpace(parts[0]) dependencies_str := strings.TrimSpace(parts[1]) - dependencies := strings.Split(dependencies_str, " ") + dependencies := strings.Fields(dependencies_str) stages = append(stages, Stage{Target: target, Dependencies: dependencies}) } else if strings.HasPrefix(line, "\t") { // command line @@ -68,3 +118,19 @@ func ParseMakefile(path string) (*Makefile, error) { } return &Makefile{Stages: stages}, nil } + +func ExecuteMakefile(path string) error { + makefile, err := ParseMakefile(path) + if err != nil { + return err + } + + ordered, err := dependencyResolver(makefile.Stages) + if err != nil { + return err + } + fmt.Println("Execution order:", ordered) + + return nil + +} From ee2a043d9be192e2003a2c68da9c7c7d7e5f9069 Mon Sep 17 00:00:00 2001 From: RawanMostafa08 Date: Mon, 15 Sep 2025 12:42:30 +0300 Subject: [PATCH 03/13] refactor: edit project structure --- pkg/depsHandler.go | 48 ++++++++++++++++++++++++++++++++++ pkg/executer.go | 19 ++++++++++++++ pkg/parser.go | 65 +--------------------------------------------- 3 files changed, 68 insertions(+), 64 deletions(-) create mode 100644 pkg/depsHandler.go create mode 100644 pkg/executer.go diff --git a/pkg/depsHandler.go b/pkg/depsHandler.go new file mode 100644 index 0000000..0903cb4 --- /dev/null +++ b/pkg/depsHandler.go @@ -0,0 +1,48 @@ +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 dependencyResolver(stages []Stage) ([]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(target, graph, visited, stack, &orderedTargets) + if err != nil { + return nil, err + } + } + } + return orderedTargets, nil +} +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 +} diff --git a/pkg/executer.go b/pkg/executer.go new file mode 100644 index 0000000..0c7da6b --- /dev/null +++ b/pkg/executer.go @@ -0,0 +1,19 @@ +package makego + +import "fmt" + +func ExecuteMakefile(path string) error { + makefile, err := parseMakefile(path) + if err != nil { + return err + } + + ordered, err := dependencyResolver(makefile.Stages) + if err != nil { + return err + } + fmt.Println("Execution order:", ordered) + + return nil + +} diff --git a/pkg/parser.go b/pkg/parser.go index 4e8103e..8ab6561 100644 --- a/pkg/parser.go +++ b/pkg/parser.go @@ -5,7 +5,6 @@ import ( "fmt" "os" "path/filepath" - "slices" "strings" ) @@ -35,53 +34,7 @@ func readMakefile(path string) (string, error) { return string(data), nil } -func buildGraph(stages []Stage) map[string][]string { - graph := make(map[string][]string) - for _, s := range stages { - graph[s.Target] = s.Dependencies - } - return graph -} - -func dependencyResolver(stages []Stage) ([]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(target, graph, visited, stack, &orderedTargets) - if err != nil { - return nil, err - } - } - } - return orderedTargets, nil -} - -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 ParseMakefile(path string) (*Makefile, error) { +func parseMakefile(path string) (*Makefile, error) { content, err := readMakefile(path) if err != nil { return nil, err @@ -118,19 +71,3 @@ func ParseMakefile(path string) (*Makefile, error) { } return &Makefile{Stages: stages}, nil } - -func ExecuteMakefile(path string) error { - makefile, err := ParseMakefile(path) - if err != nil { - return err - } - - ordered, err := dependencyResolver(makefile.Stages) - if err != nil { - return err - } - fmt.Println("Execution order:", ordered) - - return nil - -} From 1f5b558d9ecb7f49d7db0fa3b5019600763b7fbd Mon Sep 17 00:00:00 2001 From: RawanMostafa08 Date: Mon, 15 Sep 2025 14:33:37 +0300 Subject: [PATCH 04/13] test: parser unit tests --- pkg/parser.go | 17 ++++-------- pkg/parser_test.go | 56 ++++++++++++++++++++++++++++++++++++++ testdata/makefile | 2 +- testdata/makefile_invalid1 | 4 +++ testdata/makefile_invalid2 | 9 ++++++ 5 files changed, 75 insertions(+), 13 deletions(-) create mode 100644 pkg/parser_test.go create mode 100644 testdata/makefile_invalid1 create mode 100644 testdata/makefile_invalid2 diff --git a/pkg/parser.go b/pkg/parser.go index 8ab6561..3de8084 100644 --- a/pkg/parser.go +++ b/pkg/parser.go @@ -2,13 +2,12 @@ package makego import ( "errors" - "fmt" "os" - "path/filepath" "strings" ) var ErrCircularDependency = errors.New("circular dependency detected") +var ErrInvalidMakefile = errors.New("invalid makefile format") type Stage struct { Target string @@ -21,11 +20,6 @@ type Makefile struct { } func readMakefile(path string) (string, error) { - filename := filepath.Base(path) - if strings.ToLower(filename) != "makefile" { - return "", fmt.Errorf("the specified file is not a Makefile") - } - data, err := os.ReadFile(path) if err != nil { return "", err @@ -41,8 +35,7 @@ func parseMakefile(path string) (*Makefile, error) { } lines := strings.Split(content, "\n") var stages []Stage - for i, line := range lines { - // line = strings.TrimSpace(line) + for _, line := range lines { if line == "" || strings.HasPrefix(line, "#") { continue } @@ -50,7 +43,7 @@ func parseMakefile(path string) (*Makefile, error) { if strings.Contains(line, ":") { // target line parts := strings.Split(line, ":") if len(parts) != 2 { - return nil, fmt.Errorf("syntax error on line %d: %s", i+1, line) + return nil, ErrInvalidMakefile } target := strings.TrimSpace(parts[0]) @@ -60,13 +53,13 @@ func parseMakefile(path string) (*Makefile, error) { } else if strings.HasPrefix(line, "\t") { // command line if len(stages) == 0 { - return nil, fmt.Errorf("command without target on line %d: %s", i+1, line) + return nil, ErrInvalidMakefile } command := strings.TrimSpace(line) currStage := &stages[len(stages)-1] currStage.Commands = append(currStage.Commands, command) } else { - return nil, fmt.Errorf("unrecognized line format on line %d: %s", i+1, line) + return nil, ErrInvalidMakefile } } return &Makefile{Stages: stages}, nil diff --git a/pkg/parser_test.go b/pkg/parser_test.go new file mode 100644 index 0000000..b6c8ef7 --- /dev/null +++ b/pkg/parser_test.go @@ -0,0 +1,56 @@ +package makego + +import ( + "reflect" + "testing" +) + +func TestParseMakefile(t *testing.T) { + testcases := []struct { + name string + path string + expectError error + expectedFile *Makefile + }{ + { + name: "Invalid Makefile format, more than one colon in target line", + path: "../testdata/makefile_invalid1", + expectError: ErrInvalidMakefile, + expectedFile: nil, + }, + { + name: "Invalid Makefile format, command without target", + path: "../testdata/makefile_invalid2", + expectError: ErrInvalidMakefile, + expectedFile: nil, + }, + { + name: "Valid Makefile", + path: "../testdata/makefile", + expectError: nil, + expectedFile: &Makefile{ + Stages: []Stage{ + {Target: "hello", Dependencies: []string{"hello1.txt", "hello2.txt"}, Commands: []string{"cat hello1.txt", "cat hello2.txt"}}, + {Target: "hello1.txt", Dependencies: []string{}, Commands: []string{`echo "Hello 1, Make!" > hello1.txt`}}, + {Target: "hello2.txt", Dependencies: []string{}, Commands: []string{`echo "Hello 2, Make!" > hello2.txt`}}, + {Target: "clean", Dependencies: []string{}, Commands: []string{"rm -f hello1.txt", "rm -f hello2.txt"}}, + }, + }}, + } + for _, tc := range testcases { + t.Run(tc.path, func(t *testing.T) { + makefile, err := parseMakefile(tc.path) + if err != tc.expectError { + t.Errorf("expected makefile: %v, got: %v", tc.expectError, err) + } + + if makefile == nil && tc.expectedFile == nil { + return + } + + if !reflect.DeepEqual(makefile, tc.expectedFile) { + t.Fatalf("expected makefile: %+v, got: %+v", tc.expectedFile, makefile) + } + }) + } +} diff --git a/testdata/makefile b/testdata/makefile index 5c01ab3..51cd683 100644 --- a/testdata/makefile +++ b/testdata/makefile @@ -1,7 +1,7 @@ hello: hello1.txt hello2.txt cat hello1.txt cat hello2.txt - +#comment line hello1.txt: echo "Hello 1, Make!" > hello1.txt diff --git a/testdata/makefile_invalid1 b/testdata/makefile_invalid1 new file mode 100644 index 0000000..b92fd35 --- /dev/null +++ b/testdata/makefile_invalid1 @@ -0,0 +1,4 @@ +hello: hello1.txt : hello2.txt + cat hello1.txt + cat hello2.txt + \ No newline at end of file diff --git a/testdata/makefile_invalid2 b/testdata/makefile_invalid2 new file mode 100644 index 0000000..7d7b896 --- /dev/null +++ b/testdata/makefile_invalid2 @@ -0,0 +1,9 @@ +hello: hello1.txt hello2.txt + cat hello1.txt + cat hello2.txt + +hello1.txt: + echo "Hello 1, Make!" > hello1.txt + +echo "Hello 2, Make!" > hello2.txt + From 974002e6323f4a1b7ee952ea0efe5bec06c31ccf Mon Sep 17 00:00:00 2001 From: RawanMostafa08 Date: Mon, 15 Sep 2025 14:47:05 +0300 Subject: [PATCH 05/13] test: dependency resolver unit tests --- pkg/depsHandler.go | 33 +++++++++++++------------- pkg/deps_test.go | 58 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 16 deletions(-) create mode 100644 pkg/deps_test.go diff --git a/pkg/depsHandler.go b/pkg/depsHandler.go index 0903cb4..a5679fd 100644 --- a/pkg/depsHandler.go +++ b/pkg/depsHandler.go @@ -10,22 +10,6 @@ func buildGraph(stages []Stage) map[string][]string { return graph } -func dependencyResolver(stages []Stage) ([]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(target, graph, visited, stack, &orderedTargets) - if err != nil { - return nil, err - } - } - } - return orderedTargets, nil -} func dfs(node string, graph map[string][]string, visited map[string]bool, stack []string, orderedTargets *[]string) error { if visited[node] { return nil @@ -46,3 +30,20 @@ func dfs(node string, graph map[string][]string, visited map[string]bool, stack *orderedTargets = append(*orderedTargets, node) return nil } + +func dependencyResolver(stages []Stage) ([]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(target, graph, visited, stack, &orderedTargets) + if err != nil { + return nil, err + } + } + } + return orderedTargets, nil +} diff --git a/pkg/deps_test.go b/pkg/deps_test.go new file mode 100644 index 0000000..73c3c2c --- /dev/null +++ b/pkg/deps_test.go @@ -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) + 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) + } + }) + } +} From 2e029a69dd8d14ca3a4fbb1ea9e713c2bc356588 Mon Sep 17 00:00:00 2001 From: RawanMostafa08 Date: Mon, 15 Sep 2025 15:12:54 +0300 Subject: [PATCH 06/13] feat: execute initial version --- cmd/main.go | 5 ----- pkg/executer.go | 42 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index f5a5d02..7e00fa8 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -23,11 +23,6 @@ func PrintRules(rules []makego.Stage) { } } func main() { - // makefile, err := makego.ParseMakefile("testdata/makefile") - // if err != nil { - // panic(err) - // } - // PrintRules(makefile.Stages) err := makego.ExecuteMakefile("testdata/makefile") if err != nil { panic(err) diff --git a/pkg/executer.go b/pkg/executer.go index 0c7da6b..9808d19 100644 --- a/pkg/executer.go +++ b/pkg/executer.go @@ -1,6 +1,20 @@ package makego -import "fmt" +import ( + "fmt" + "os" + "os/exec" + "strings" +) + +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) error { makefile, err := parseMakefile(path) @@ -12,7 +26,31 @@ func ExecuteMakefile(path string) error { if err != nil { return err } - fmt.Println("Execution order:", ordered) + + for _, target := range ordered { + stage := getStageByTarget(makefile.Stages, target) + if stage == nil { + continue + } + for _, cmd := range stage.Commands { + printCmd := true + if strings.HasPrefix(strings.TrimSpace(cmd), "echo") && + (strings.Contains(cmd, ">") || strings.Contains(cmd, ">>")) { + printCmd = false + } + + if printCmd { + fmt.Println(cmd) + } + cmd := exec.Command("sh", "-c", cmd) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := cmd.Run() + if err != nil { + return err + } + } + } return nil From 250c50eba53bacce1f9595deca0e19c9b6f51b2c Mon Sep 17 00:00:00 2001 From: RawanMostafa08 Date: Mon, 15 Sep 2025 16:38:51 +0300 Subject: [PATCH 07/13] fix: execute first stage only --- pkg/depsHandler.go | 4 ++-- pkg/deps_test.go | 2 +- pkg/executer.go | 14 ++++++++++++-- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/pkg/depsHandler.go b/pkg/depsHandler.go index a5679fd..c93a9d4 100644 --- a/pkg/depsHandler.go +++ b/pkg/depsHandler.go @@ -31,7 +31,7 @@ func dfs(node string, graph map[string][]string, visited map[string]bool, stack return nil } -func dependencyResolver(stages []Stage) ([]string, error) { +func dependencyResolver(stages []Stage, root string) ([]string, error) { graph := buildGraph(stages) visited := make(map[string]bool) stack := make([]string, 0) @@ -39,7 +39,7 @@ func dependencyResolver(stages []Stage) ([]string, error) { for target := range graph { if !visited[target] { - err := dfs(target, graph, visited, stack, &orderedTargets) + err := dfs(root, graph, visited, stack, &orderedTargets) if err != nil { return nil, err } diff --git a/pkg/deps_test.go b/pkg/deps_test.go index 73c3c2c..a93a660 100644 --- a/pkg/deps_test.go +++ b/pkg/deps_test.go @@ -45,7 +45,7 @@ func TestDependencyResolver(t *testing.T) { } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { - ordered, err := dependencyResolver(tc.stages) + ordered, err := dependencyResolver(tc.stages, tc.stages[0].Target) if err != tc.expectError { t.Errorf("expected error: %v, got: %v", tc.expectError, err) } diff --git a/pkg/executer.go b/pkg/executer.go index 9808d19..21f4eb6 100644 --- a/pkg/executer.go +++ b/pkg/executer.go @@ -22,12 +22,22 @@ func ExecuteMakefile(path string) error { return err } - ordered, err := dependencyResolver(makefile.Stages) + ordered, err := dependencyResolver(makefile.Stages, makefile.Stages[0].Target) if err != nil { return err } - for _, target := range ordered { + defaultTarget := makefile.Stages[0].Target + runTargets := []string{} + + for _, t := range ordered { //run the first target and its dependencies only + runTargets = append(runTargets, t) + if t == defaultTarget { + break + } + } + + for _, target := range runTargets { stage := getStageByTarget(makefile.Stages, target) if stage == nil { continue From d6cde24fff012cf146e0e6658e5a6f9a318e23fd Mon Sep 17 00:00:00 2001 From: RawanMostafa08 Date: Mon, 15 Sep 2025 16:57:24 +0300 Subject: [PATCH 08/13] feat: added targets s CL arguments, fixed a logic issue in the execution --- cmd/main.go | 11 +++++++++- pkg/executer.go | 53 ++++++++++++++++++++----------------------------- 2 files changed, 31 insertions(+), 33 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 7e00fa8..392c48a 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "os" "strings" makego "github.com/codescalersinternships/makego-rawan/pkg" @@ -23,7 +24,15 @@ func PrintRules(rules []makego.Stage) { } } func main() { - err := makego.ExecuteMakefile("testdata/makefile") + args := os.Args[1:] + + var targets []string + targets = nil + if len(args) > 0 { + targets = args[0:] + } + + err := makego.ExecuteMakefile("testdata/makefile", targets) if err != nil { panic(err) } diff --git a/pkg/executer.go b/pkg/executer.go index 21f4eb6..df59bea 100644 --- a/pkg/executer.go +++ b/pkg/executer.go @@ -4,7 +4,6 @@ import ( "fmt" "os" "os/exec" - "strings" ) func getStageByTarget(stages []Stage, target string) *Stage { @@ -16,50 +15,40 @@ func getStageByTarget(stages []Stage, target string) *Stage { return nil } -func ExecuteMakefile(path string) error { +func ExecuteMakefile(path string, targets []string) error { makefile, err := parseMakefile(path) if err != nil { return err } - ordered, err := dependencyResolver(makefile.Stages, makefile.Stages[0].Target) - if err != nil { - return err - } - defaultTarget := makefile.Stages[0].Target - runTargets := []string{} - for _, t := range ordered { //run the first target and its dependencies only - runTargets = append(runTargets, t) - if t == defaultTarget { - break - } + if len(targets) == 0 { + targets = []string{defaultTarget} } - for _, target := range runTargets { - stage := getStageByTarget(makefile.Stages, target) - if stage == nil { - continue + for _, target := range targets { + ordered, err := dependencyResolver(makefile.Stages, target) + if err != nil { + return err } - for _, cmd := range stage.Commands { - printCmd := true - if strings.HasPrefix(strings.TrimSpace(cmd), "echo") && - (strings.Contains(cmd, ">") || strings.Contains(cmd, ">>")) { - printCmd = false - } - - if printCmd { - fmt.Println(cmd) + for _, t := range ordered { + stage := getStageByTarget(makefile.Stages, t) + if stage == nil { + continue } - cmd := exec.Command("sh", "-c", cmd) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - err := cmd.Run() - if err != nil { - return err + 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 From 9ab6a1477b94072450c0ff7f490e6d0b474feab6 Mon Sep 17 00:00:00 2001 From: RawanMostafa08 Date: Mon, 15 Sep 2025 17:07:04 +0300 Subject: [PATCH 09/13] feat: added -f flag to specify the makefile --- cmd/main.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 392c48a..27fc087 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,8 +1,8 @@ package main import ( + "flag" "fmt" - "os" "strings" makego "github.com/codescalersinternships/makego-rawan/pkg" @@ -24,15 +24,19 @@ func PrintRules(rules []makego.Stage) { } } func main() { - args := os.Args[1:] + 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("testdata/makefile", targets) + err := makego.ExecuteMakefile(*file, targets) if err != nil { panic(err) } From ef091948f8ca55e77f34c6b894c2aabf81835020 Mon Sep 17 00:00:00 2001 From: RawanMostafa08 Date: Tue, 16 Sep 2025 09:23:09 +0300 Subject: [PATCH 10/13] feat: integration test for the execute function which calls parser,deps resolver, execution --- pkg/{deps_test.go => depsHandler_test.go} | 0 pkg/executer.go | 2 - pkg/executer_test.go | 97 +++++++++++++++++++++++ 3 files changed, 97 insertions(+), 2 deletions(-) rename pkg/{deps_test.go => depsHandler_test.go} (100%) create mode 100644 pkg/executer_test.go diff --git a/pkg/deps_test.go b/pkg/depsHandler_test.go similarity index 100% rename from pkg/deps_test.go rename to pkg/depsHandler_test.go diff --git a/pkg/executer.go b/pkg/executer.go index df59bea..2aba9b5 100644 --- a/pkg/executer.go +++ b/pkg/executer.go @@ -20,7 +20,6 @@ func ExecuteMakefile(path string, targets []string) error { if err != nil { return err } - defaultTarget := makefile.Stages[0].Target if len(targets) == 0 { @@ -52,5 +51,4 @@ func ExecuteMakefile(path string, targets []string) error { } return nil - } diff --git a/pkg/executer_test.go b/pkg/executer_test.go new file mode 100644 index 0000000..c5ec480 --- /dev/null +++ b/pkg/executer_test.go @@ -0,0 +1,97 @@ +package makego + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func writeMakefile(t *testing.T, content string) string { + t.Helper() + + tmpDir := t.TempDir() + path := filepath.Join(tmpDir, "Makefile") + + if err := os.WriteFile(path, []byte(content), 0644); err != nil { + t.Fatalf("failed to write temp Makefile: %v", err) + } + + return path +} + +func TestExecuteMakefileIntegration(t *testing.T) { + t.Run("default target with dependencies only", func(t *testing.T) { + makefileContent := ` +hello: hello1 hello2 + cat hello1.txt + cat hello2.txt + +hello1: + echo "Hello 1, Make!" > hello1.txt + +hello2: + echo "Hello 2, Make!" > hello2.txt + +clean: + rm -f hello1.txt hello2.txt +` + path := writeMakefile(t, makefileContent) + + err := ExecuteMakefile(path, []string{}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + data, err := os.ReadFile("hello1.txt") + if err != nil { + t.Fatalf("expected hello1.txt to exist %v", err) + } + if !strings.Contains(string(data), "Hello 1, Make!") { + t.Errorf("unexpected hello1.txt content, got %q", string(data)) + } + + data, err = os.ReadFile("hello2.txt") + if err != nil { + t.Fatalf("expected hello2.txt to exist %v", err) + } + if !strings.Contains(string(data), "Hello 2, Make!") { + t.Errorf("unexpected hello2.txt content, got %q", string(data)) + } + }) + + t.Run("run target clean", func(t *testing.T) { + makefileContent := ` +clean: + rm -f dummy.txt +` + path := writeMakefile(t, makefileContent) + + if err := os.WriteFile("dummy.txt", []byte{}, 0644); err != nil { + t.Fatalf("failed to write dummy file: %v", err) + } + + err := ExecuteMakefile(path, []string{"clean"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + _, err = os.Stat("dummy.txt") + if !os.IsNotExist(err) { + t.Errorf("clean target didn't remove dummy file") + } + }) + + t.Run("fails on invalid command", func(t *testing.T) { + makefileContent := ` +invalid: + invalidcommand +` + path := writeMakefile(t, makefileContent) + + err := ExecuteMakefile(path, []string{"invalid"}) + if err == nil { + t.Fatalf("expected error for invalid command, got nil") + } + }) +} From 07d04cbd9629b08a0f9eed901a5067d8e0ac790f Mon Sep 17 00:00:00 2001 From: RawanMostafa08 Date: Tue, 16 Sep 2025 13:13:46 +0300 Subject: [PATCH 11/13] feat: concurrency and synchronization --- pkg/executer.go | 73 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 58 insertions(+), 15 deletions(-) diff --git a/pkg/executer.go b/pkg/executer.go index 2aba9b5..21c3aee 100644 --- a/pkg/executer.go +++ b/pkg/executer.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "os/exec" + "sync" ) func getStageByTarget(stages []Stage, target string) *Stage { @@ -27,28 +28,70 @@ func ExecuteMakefile(path string, targets []string) error { } for _, target := range targets { - ordered, err := dependencyResolver(makefile.Stages, target) + ordered, err := dependencyResolver(makefile.Stages, target) // get the ordered list of targets to execute if err != nil { return err } - for _, t := range ordered { - stage := getStageByTarget(makefile.Stages, t) - if stage == nil { - continue - } - 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 + + 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 } From 2c4c29911eaa5e37f6d8d7971dfebc17d54b3f0b Mon Sep 17 00:00:00 2001 From: RawanMostafa08 Date: Tue, 16 Sep 2025 13:57:00 +0300 Subject: [PATCH 12/13] doc: user documentation --- README.md | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a0973fc..0445de5 100644 --- a/README.md +++ b/README.md @@ -1 +1,77 @@ -# makego-rawan \ No newline at end of file +# 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 + +### 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 + +- `` : The target to be run (optional) + - default: first target in the makefile + +## Concurrency + +- Independent targets run in parallel +- Channels are used to handle errors \ No newline at end of file From acd41f3ad9b6221beaa41f382e2116c09e26dee7 Mon Sep 17 00:00:00 2001 From: RawanMostafa08 Date: Tue, 16 Sep 2025 14:01:26 +0300 Subject: [PATCH 13/13] doc: added code example to user doc --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 0445de5..ce98156 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ This repository provides a Go-based tool to execute Makefile targets with suppor - Ordered execution for dependent stages. - Error handling through channels for safe concurrent execution. + ## Installation 1. Clone this repository: @@ -20,6 +21,19 @@ This repository provides a Go-based tool to execute Makefile targets with suppor ## 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