Features
Command-Line Interface: Implements a command-line interface using cobra
.
Makefile Parsing: Parses simple Makefile formats to extract tasks and dependencies.
Task Execution: Executes tasks and commands defined in the Makefile.
Variable Substitution: Supports substituting variables within commands.
Dependency Resolution: Handles task dependencies to ensure proper execution order.
Cross-Platform Support: Provides compatibility for both Windows and Unix-like systems.
Logging: Includes basic logging for better debugging and task tracking.
Stack Implementation: Utilizes a stack to manage conditional logic.
Error Handling: Gracefully handles errors to ensure robust execution.
Modular Structure: Organizes code into logical and reusable modules.
Each feature includes a rudimentary implementation to demonstrate the fundamentals of creating a make
-like program. The structure is designed to be expandable and adaptable to accommodate additional requirements or complexities.
Basic support for conditional logic (ifeq
, ifneq
, ifdef
, ifndef
) is provided. More advanced features can be integrated as needed.
Global Variables in Makefiles
The following global variables are supported:
OS
: Operating system name
PWD
: Current working directory
CURDIR
: Current working directory
HOME
: Home directory
SHELL
: Shell program name
PATH
: System path
USER
: Current user
USERNAME
: Current user name
MAKE
: Make command
Additional variables can be defined and used as required.
Code Implementation
main.go
package main
import (
"fmt"
"github.com/username/go-make/vars"
"os"
"github.com/username/go-make/cmd"
)
func main() {
vars.Init()
if err := cmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
cmd/root.go
package cmd
import (
"fmt"
"log"
"os"
"strings"
"github.com/username/go-make/executor"
"github.com/username/go-make/vars"
"github.com/spf13/cobra"
"github.com/username/go-make/make_file"
)
var rootCmd = &cobra.Command{
Use: "gomake",
Short: "GoMake is a lightweight alternative to GNU Make",
Long: `GoMake parses a simple Makefile and executes the tasks defined in it.`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) < 1 {
fmt.Println("Please specify a target task")
os.Exit(1)
}
taskName := ""
for _, arg := range args {
if strings.Contains(arg, "=") {
parts := strings.Split(arg, "=")
vars.VarList[parts[0]] = vars.EvalVar(parts[1])
} else if len(taskName) == 0 {
taskName = arg
} else {
log.Fatal("Too many arguments")
os.Exit(1)
}
}
makeFileName, _ := cmd.Flags().GetString("file")
if len(makeFileName) == 0 {
makeFileName = "Makefile"
}
tasks, err := make_file.ParseMakefile(makeFileName)
if err != nil {
fmt.Printf("Error parsing Makefile: %v\n", err)
os.Exit(1)
}
if err := executor.RunTask(taskName, tasks); err != nil {
fmt.Printf("Error executing task: %v\n", err)
os.Exit(1)
}
},
}
func Execute() error {
rootCmd.Flags().StringP("file", "f", "Makefile", "Specify the Makefile to use")
return rootCmd.Execute()
}
make_file/parser.go
package make_file
import (
"bufio"
"fmt"
"github.com/username/go-make/utils"
"log"
"os"
"regexp"
"strings"
"github.com/username/go-make/vars"
)
type Task struct {
Name string
Dependencies []string
Command []string
}
func ParseLogic(line string) bool {
// Remove leading stuff until the first parenthesis
pos := strings.Index(line, "(")
if pos == -1 {
return false
}
stmt := strings.Trim(line[:pos], " ")
rawLogic := line[pos+1:]
logic := ""
idx := 0
pCount := 0
for idx < len(rawLogic) {
if rawLogic[idx] == '(' {
pCount++
} else if rawLogic[idx] == ')' {
pCount--
if pCount < 0 {
logic = strings.Trim(rawLogic[:idx], " ")
break
}
}
idx++
}
re := regexp.MustCompile(`\$\([a-zA-Z0-9_-]+\)`)
for re.MatchString(logic) {
logic = re.ReplaceAllStringFunc(logic, func(match string) string {
// Replace the match with some value, for example, the key itself without $()
key := match[2 : len(match)-1]
value, ok := vars.VarList[key]
if !ok {
log.Fatalf("Variable %s not found", key)
}
return fmt.Sprintf("%s", value) // Example replacement
})
}
// Evaluate the logic
left := strings.Trim(strings.Split(logic, ",")[0], " ")
right := strings.Trim(strings.Split(logic, ",")[1], " ")
if stmt == "ifeq" {
return left == right
} else if stmt == "ifneq" {
return left != right
} else if stmt == "ifdef" {
_, ok := vars.VarList[left]
return ok
} else if stmt == "ifndef" {
_, ok := vars.VarList[left]
return !ok
}
return false
}
// ParseMakefile reads and parses a Makefile.
func ParseMakefile(filename string) (map[string]*Task, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()
tasks := make(map[string]*Task)
scanner := bufio.NewScanner(file)
var currentTask *Task
logicStack := utils.Stack{}
for scanner.Scan() {
line := scanner.Text()
trimmedLine := strings.TrimSpace(line)
if strings.HasPrefix(trimmedLine, "ifeq") {
logicStack.Push(ParseLogic(trimmedLine))
} else if strings.HasPrefix(trimmedLine, "ifneq") {
logicStack.Push(ParseLogic(trimmedLine))
} else if strings.HasPrefix(trimmedLine, "ifdef") {
logicStack.Push(ParseLogic(trimmedLine))
} else if strings.HasPrefix(trimmedLine, "ifndef") {
logicStack.Push(ParseLogic(trimmedLine))
} else if strings.HasPrefix(trimmedLine, "else") {
logicStack.InvertCurrent()
} else if strings.HasPrefix(trimmedLine, "endif") {
if logicStack.IsEmpty() {
log.Fatalf("Unexpected endif in line: %s", line)
}
logicStack.Pop()
}
if !logicStack.IsEmpty() {
if value, ok := logicStack.Peek(); ok && !value {
continue
}
}
if !strings.HasPrefix(line, "\t") {
if strings.Contains(line, "=") {
parts := strings.SplitN(line, "=", 2)
vars.VarList[parts[0]] = vars.EvalVar(parts[1])
continue
}
parts := strings.Split(trimmedLine, ":")
taskName := strings.TrimSuffix(parts[0], ":")
currentTask = &Task{Name: taskName}
tasks[taskName] = currentTask
if len(parts) > 1 && parts[1] != "" {
currentTask.Dependencies = strings.Fields(parts[1])
}
} else if currentTask != nil && strings.HasPrefix(line, "\t") {
currentTask.Command = append(currentTask.Command, strings.TrimSpace(line))
} else if currentTask != nil {
currentTask.Dependencies = append(currentTask.Dependencies, strings.Fields(line)...)
} else {
log.Fatalf("Unknown action in line: %s", line)
}
}
if err := scanner.Err(); err != nil {
return nil, err
}
return tasks, nil
}
make_file/task.go
package make_file
import "fmt"
// ResolveDependencies resolves all dependencies for a task recursively.
func ResolveDependencies(taskName string, tasks map[string]*Task, resolved map[string]bool) ([]string, error) {
if resolved[taskName] {
return nil, nil
}
task, exists := tasks[taskName]
if !exists {
return nil, fmt.Errorf("task '%s' not found", taskName)
}
var dependencies []string
for _, dep := range task.Dependencies {
subDeps, err := ResolveDependencies(dep, tasks, resolved)
if err != nil {
return nil, err
}
dependencies = append(dependencies, subDeps...)
}
resolved[taskName] = true
dependencies = append(dependencies, taskName)
return dependencies, nil
}
executor/runner.go
package executor
import (
"fmt"
"os"
"os/exec"
"runtime"
"strings"
"github.com/username/go-make/make_file"
"github.com/username/go-make/vars"
)
func RunTask(taskName string, tasks map[string]*make_file.Task) error {
resolved := make(map[string]bool)
executionOrder, err := make_file.ResolveDependencies(taskName, tasks, resolved)
if err != nil {
return err
}
for _, task := range executionOrder {
if len(tasks[task].Command) > 0 {
for _, command := range tasks[task].Command {
command = vars.EvalVar(command)
fmt.Println(command)
var cmd *exec.Cmd
if runtime.GOOS == "windows" {
cmd = exec.Command("cmd", "/C", strings.TrimPrefix(command, "@"))
} else {
cmd = exec.Command("sh", "-c", strings.TrimPrefix(command, "@"))
}
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
if err := cmd.Run(); err != nil {
return fmt.Errorf("task '%s' failed: %s", task, err.Error())
}
}
}
}
return nil
}
utils/logger.go
package utils
import (
"log"
"os"
)
var logger = log.New(os.Stdout, "[GoMake] ", log.LstdFlags)
// Info logs an informational message.
func Info(msg string) {
logger.Println(msg)
}
// Error logs an error message.
func Error(msg string) {
logger.Println(msg)
}
utils/stack.go
package utils
// Stack represents a dynamic stack of bool values
type Stack []bool
// Push appends a value to the stack
func (s *Stack) Push(value bool) {
*s = append(*s, value)
}
// Pop removes and returns the last value from the stack
func (s *Stack) Pop() (bool, bool) {
if len(*s) == 0 {
// Handle empty stack
return false, false
}
last := (*s)[len(*s)-1]
*s = (*s)[:len(*s)-1]
return last, true
}
// Peek returns the last value from the stack without removing it
func (s *Stack) Peek() (bool, bool) {
if len(*s) == 0 {
// Handle empty stack
return false, false
}
return (*s)[len(*s)-1], true
}
// IsEmpty returns true if the stack is empty
func (s *Stack) IsEmpty() bool {
return len(*s) == 0
}
// InvertCurrent inverts the last value in the stack
func (s *Stack) InvertCurrent() {
if len(*s) == 0 {
// Handle empty stack
return
}
(*s)[len(*s)-1] = !(*s)[len(*s)-1]
}