package main import ( "fmt" "os" "os/exec" "path/filepath" "strconv" "strings" ) func main() { // Ensure there's a make target specified. if len(os.Args) < 2 { fmt.Println("Usage: fdm [--option=value ...] -- [make-options]") os.Exit(1) } // Determine the path to the top-level directory (where the Makefile resides). repoRoot, err := getRepoRoot() if err != nil { fmt.Printf("Error determining repo root: %v\n", err) os.Exit(1) } // Change the working directory to the repo root. if err := os.Chdir(repoRoot); err != nil { fmt.Printf("Error changing directory to repo root: %v\n", err) os.Exit(1) } // Extract the make target. makeTarget := os.Args[1] // Split arguments into options and make arguments. options, makeArgs := splitArgs(os.Args[2:]) // Special logic for the help command. if makeTarget == "help" { if len(os.Args) > 2 && !strings.HasPrefix(os.Args[2], "--") { options["REFORMAT_OPTIONS"] = "true" } else { fmt.Println("\033[1mNAME\033[0m") fmt.Println(" fdm - developer tools for fleet device management") fmt.Println() fmt.Println("\033[1mUSAGE:\033[0m") fmt.Println(" fdm [--option=value ...] -- [make-options]") fmt.Println() fmt.Println("\033[1mCOMMANDS:\033[0m") options["HELP_CMD_PREFIX"] = "fdm" } } // Transform options into Makefile-compatible variables. makeVars := transformToMakeVars(options) makeVars = append(makeVars, "TOOL_CMD=fdm") // Call the Makefile with the specified target, Make variables, and additional arguments. err = callMake(makeTarget, makeVars, makeArgs) if err != nil { fmt.Printf("Error: %v\n", err) os.Exit(1) } } // splitArgs splits the arguments into options and make arguments based on the `--` delimiter. func splitArgs(args []string) (map[string]string, []string) { options := make(map[string]string) var makeArgs []string cliArgs := " " positionalArgsIndex := 1 isMakeArgs := false skipNext := false for idx, arg := range args { if skipNext { skipNext = false continue } if arg == "--" { isMakeArgs = true continue } switch { // If we're processing make args (anything after a bare -- ) // then add the current arg to the list. case isMakeArgs: makeArgs = append(makeArgs, arg) // Otherwise if the arg has a -- prefix, treat it like an option // for the command. case strings.HasPrefix(arg, "--"): // Remove "--" and split by "=". parts := strings.SplitN(arg[2:], "=", 2) switch { // Handle options like --name=foo case len(parts) == 2: options[parts[0]] = parts[1] cliArgs += arg + " " // Handle options like --name foo case idx+1 < len(args) && !strings.HasPrefix(args[idx+1], "--"): options[arg[2:]] = args[idx+1] cliArgs += arg + " " + args[idx+1] + " " skipNext = true // Handle options like --useturbocharge by assuming they're booleans. default: options[parts[0]] = "true" cliArgs += arg + " " } // Otherwise assume we're dealing with a positional argument. default: options["arg"+strconv.Itoa(positionalArgsIndex)] = arg positionalArgsIndex++ } } options["CLI_ARGS"] = cliArgs return options, makeArgs } // transformToMakeVars converts kebab-cased options to snake-cased Makefile variables. func transformToMakeVars(options map[string]string) []string { var makeVars []string for key, value := range options { // Convert kebab-case to snake_case and uppercase. varName := strings.ToUpper(strings.ReplaceAll(key, "-", "_")) makeVars = append(makeVars, fmt.Sprintf("%s=%s", varName, value)) } return makeVars } // callMake invokes the `make` command with the given target, variables, and additional arguments. func callMake(target string, makeVars []string, makeArgs []string) error { // Construct the command with target and makeArgs. finalArgs := []string{target} finalArgs = append(finalArgs, makeVars...) finalArgs = append(finalArgs, makeArgs...) cmd := exec.Command("make", finalArgs...) // Use the same stdin, stdout, and stderr as the parent process. cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr // Run the command. return cmd.Run() } // getRepoRoot determines the repo root (top-level directory) relative to this binary. func getRepoRoot() (string, error) { // Get the path of the currently executing binary executable, err := os.Executable() if err != nil { return "", err } // Get the path of the binary, following symlinks. execDir, err := filepath.EvalSymlinks(executable) if err != nil { return "", err } // Get the directory. execDir = filepath.Dir(execDir) // Compute the repo root relative to the binary's location. repoRoot := filepath.Join(execDir, "../") return filepath.Abs(repoRoot) }