package main import ( "encoding/json" "flag" "fmt" "os" "os/exec" "path/filepath" "regexp" "runtime" "strings" "gotlin/internal/lang" ) func main() { if len(os.Args) < 2 { usage() } switch os.Args[1] { case "build": runBuild(os.Args[2:]) case "run": runRun(os.Args[2:]) default: runBuild(os.Args[1:]) } } func runBuild(args []string) { fs := flag.NewFlagSet("build", flag.ExitOnError) srcOnly := fs.Bool("src", false, "emit Go source instead of building an executable") outPath := fs.String("o", "", "output file path") metadataOutput := fs.String("metadata-output", "", "write Gotlin package metadata") metadataPackage := fs.String("metadata-package", "", "import path recorded in package metadata") var metadataInputs stringListFlag fs.Var(&metadataInputs, "metadata", "Gotlin package metadata dependency (repeatable)") fs.Usage = func() { fmt.Fprintln(os.Stderr, "usage: gotlinc build [-src] [-o output] ") } normalizedArgs, err := normalizeBuildArgs(args) if err != nil { fail(err) } if err := fs.Parse(normalizedArgs); err != nil { fail(err) } if fs.NArg() < 1 { fs.Usage() os.Exit(2) } inputPath := fs.Arg(0) goSrc := compileFilesWithMetadata(fs.Args(), !*srcOnly, metadataInputs, *metadataOutput, *metadataPackage) if *srcOnly { if *outPath == "" { _, _ = os.Stdout.WriteString(goSrc) return } if err := os.WriteFile(*outPath, []byte(goSrc), 0o644); err != nil { fail(err) } return } outputPath := *outPath if outputPath == "" { outputPath = defaultExecutablePath(inputPath) } if err := buildExecutable(goSrc, outputPath); err != nil { fail(err) } } func runRun(args []string) { fs := flag.NewFlagSet("run", flag.ExitOnError) fs.Usage = func() { fmt.Fprintln(os.Stderr, "usage: gotlinc run [program args...]") } if err := fs.Parse(args); err != nil { fail(err) } if fs.NArg() < 1 { fs.Usage() os.Exit(2) } inputPath := fs.Arg(0) goSrc := compileFile(inputPath, true) tmpFile, err := os.CreateTemp("", "gotlinc-*.go") if err != nil { fail(err) } tmpPath := tmpFile.Name() defer os.Remove(tmpPath) if _, err := tmpFile.WriteString(goSrc); err != nil { _ = tmpFile.Close() fail(err) } if err := tmpFile.Close(); err != nil { fail(err) } cmdArgs := append([]string{"run", tmpPath}, fs.Args()[1:]...) cmd := exec.Command("go", cmdArgs...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Stdin = os.Stdin if err := cmd.Run(); err != nil { fail(err) } } func compileFile(inputPath string, forceMain bool) string { return compileFiles([]string{inputPath}, forceMain) } func compileFiles(inputPaths []string, forceMain bool) string { return compileFilesWithMetadata(inputPaths, forceMain, nil, "", "") } func compileFilesWithMetadata(inputPaths []string, forceMain bool, metadataPaths []string, metadataOutput, metadataPackage string) string { program := &lang.Program{} var firstSource string for _, inputPath := range inputPaths { src, err := os.ReadFile(inputPath) if err != nil { fail(err) } parsed, err := lang.Parse(string(src)) if err != nil { fail(err) } if program.PackagePath == "" { program.PackagePath = parsed.PackagePath } if parsed.PackagePath != program.PackagePath { fail(fmt.Errorf("Gotlin files must use the same package")) } program.Imports = append(program.Imports, parsed.Imports...) program.Interfaces = append(program.Interfaces, parsed.Interfaces...) program.Enums = append(program.Enums, parsed.Enums...) program.Classes = append(program.Classes, parsed.Classes...) program.Functions = append(program.Functions, parsed.Functions...) program.Embeds = append(program.Embeds, parsed.Embeds...) if firstSource == "" { firstSource = string(src) } } metadata := loadMetadata(metadataPaths) if metadataOutput != "" { packageMetadata, err := lang.BuildPackageMetadata(program, metadataPackage) if err != nil { fail(err) } encoded, err := json.MarshalIndent(packageMetadata, "", " ") if err != nil { fail(err) } encoded = append(encoded, '\n') if err := os.WriteFile(metadataOutput, encoded, 0o644); err != nil { fail(err) } } var goSrc []byte var err error if forceMain { goSrc, err = lang.GenerateGoMainWithMetadata(program, metadata) } else { goSrc, err = lang.GenerateGoWithMetadata(program, metadata) } if err != nil { fail(err) } if forceMain { return addBestEffortLineDirectives(string(goSrc), inputPaths[0], firstSource) } return string(goSrc) } type stringListFlag []string func (values *stringListFlag) String() string { return strings.Join(*values, ",") } func (values *stringListFlag) Set(value string) error { *values = append(*values, value); return nil } func loadMetadata(paths []string) []*lang.PackageMetadata { metadata := make([]*lang.PackageMetadata, 0, len(paths)) for _, path := range paths { body, err := os.ReadFile(path) if err != nil { fail(err) } var item lang.PackageMetadata if err := json.Unmarshal(body, &item); err != nil { fail(fmt.Errorf("load metadata %s: %w", path, err)) } if item.Version != lang.PackageMetadataVersion { fail(fmt.Errorf("metadata %s uses unsupported version %d", path, item.Version)) } metadata = append(metadata, &item) } return metadata } func buildExecutable(goSrc string, outputPath string) error { tmpFile, err := os.CreateTemp("", "gotlinc-build-*.go") if err != nil { return err } tmpPath := tmpFile.Name() defer os.Remove(tmpPath) if _, err := tmpFile.WriteString(goSrc); err != nil { _ = tmpFile.Close() return err } if err := tmpFile.Close(); err != nil { return err } cmd := exec.Command("go", "build", "-o", outputPath, tmpPath) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Stdin = os.Stdin return cmd.Run() } func defaultExecutablePath(inputPath string) string { base := strings.TrimSuffix(filepath.Base(inputPath), filepath.Ext(inputPath)) if runtime.GOOS == "windows" && filepath.Ext(base) != ".exe" { base += ".exe" } return base } func normalizeBuildArgs(args []string) ([]string, error) { var flags []string var positional []string for i := 0; i < len(args); i++ { arg := args[i] switch { case arg == "-src": flags = append(flags, arg) case arg == "-o" || arg == "-metadata" || arg == "-metadata-output" || arg == "-metadata-package": if i+1 >= len(args) { return nil, fmt.Errorf("missing value for -o") } flags = append(flags, arg, args[i+1]) i++ case strings.HasPrefix(arg, "-o="): flags = append(flags, arg) case strings.HasPrefix(arg, "-"): flags = append(flags, arg) default: positional = append(positional, arg) } } return append(flags, positional...), nil } func usage() { fmt.Fprintln(os.Stderr, "usage:") fmt.Fprintln(os.Stderr, " gotlinc build [-src] [-o output] ") fmt.Fprintln(os.Stderr, " gotlinc run [program args...]") fmt.Fprintln(os.Stderr, "") fmt.Fprintln(os.Stderr, "default command: build") os.Exit(2) } func fail(err error) { fmt.Fprintln(os.Stderr, err) os.Exit(1) } func addBestEffortLineDirectives(goSrc string, inputPath string, gtSrc string) string { fnLines := sourceFunctionLines(gtSrc) if len(fnLines) == 0 { return goSrc } pattern := regexp.MustCompile(`^\s*func\s+(?:\(\s*[^)]*\)\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*\(`) lines := strings.Split(goSrc, "\n") out := make([]string, 0, len(lines)+len(fnLines)) for _, line := range lines { m := pattern.FindStringSubmatch(line) if len(m) == 2 { if gtLine, ok := fnLines[m[1]]; ok { out = append(out, fmt.Sprintf("//line %s:%d", inputPath, gtLine)) } } out = append(out, line) } return strings.Join(out, "\n") } func sourceFunctionLines(src string) map[string]int { out := map[string]int{} pattern := regexp.MustCompile(`^\s*fun\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(`) lines := strings.Split(src, "\n") for i, line := range lines { m := pattern.FindStringSubmatch(line) if len(m) == 2 { if _, exists := out[m[1]]; !exists { out[m[1]] = i + 1 } } } return out }