diff --git a/.gitignore b/.gitignore index e243c20..9ba5b87 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,4 @@ c65gm opencode-config/package-lock.json internal/preproc/lib/ .bun/ +*.asm diff --git a/AGENTS.md b/AGENTS.md index ba396d2..a1c7fbb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -157,8 +157,17 @@ C65LIBPATH=/app/lib ./c65gm -in input.c65 -out output.asm ### Compilation **IMPORTANT**: The agent cannot compile code. Provide these instructions to the user: + +#### New Self-Contained Method (Recommended) ```bash -./c65gm -in input.c65 -out output.asm +./c65gm myprogram.c65 # Creates myprogram.prg (compile + assemble) +./c65gm build -i myprogram.c65 # Same as above +./c65gm compile -i myprogram.c65 # Creates myprogram.asm only +``` + +#### Legacy Method (Still Supported) +```bash +./c65gm -in myprogram.c65 -out output.asm acme -f cbm -o output.prg output.asm ``` diff --git a/README.md b/README.md index cb2c36e..b8248f3 100644 --- a/README.md +++ b/README.md @@ -42,18 +42,46 @@ go install ## Usage -Compile a source file to ACME assembly: +### Quick Start (Recommended) +Compile and assemble directly to a .prg file: ```bash -./c65gm -in input.c65 -out output.asm +./c65gm myprogram.c65 # Creates myprogram.prg +./c65gm -i myprogram.c65 -o game.prg # Creates game.prg ``` -Then assemble the output with ACME: +### Command Reference +#### Build (compile + assemble to .prg) ```bash -acme -f cbm -o output.prg output.asm +./c65gm build -i myprogram.c65 [-o output.prg] [--keep-asm] [--no-cbm] +./c65gm myprogram.c65 # Default build to myprogram.prg +./c65gm -i myprogram.c65 # Same as above +./c65gm -in myprogram.c65 # Legacy syntax, still works ``` +#### Compile (to .asm only) +```bash +./c65gm compile -i myprogram.c65 [-o output.asm] +./c65gm myprogram.c65 -o output.asm # .asm extension triggers compile mode +./c65gm -i myprogram.c65 -out output.asm # Legacy syntax +``` + +#### Help +```bash +./c65gm help +./c65gm -h +./c65gm --help +``` + +### Key Features +- **Self-contained**: No external build scripts needed +- **Flexible syntax**: `-i`/`-in` and `-o`/`-out` are equivalent +- **Smart defaults**: Output extension determines mode (.prg = build, .asm = compile) +- **ACME integration**: Automatically finds and runs ACME assembler with `-f cbm` by default +- **Backward compatible**: Legacy `-in`/`-out` flags still work +- **Customizable**: Use `--no-cbm` to disable CBM format, `--keep-asm` to keep intermediate files + ## Running Tests Run all tests: @@ -83,6 +111,16 @@ See the `examples/` directory for sample programs: - `memlib_demo/` - Memory library usage - `switch_demo/` - SWITCH/CASE statement examples +### Building Examples +```bash +cd examples/hires +c65gm hires.c65 # Creates hires.prg +c65gm -i hires.c65 -o demo.prg # Creates demo.prg +c65gm compile -i hires.c65 # Creates hires.asm only +``` + +The example directories also contain `cm.sh` scripts showing the old build method. + ## Documentation - `language.md` - Complete language reference diff --git a/main.go b/main.go index 0d0f96f..3e6856b 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,8 @@ import ( "flag" "fmt" "os" + "os/exec" + "path/filepath" "strings" "c65gm/internal/commands" @@ -21,29 +23,85 @@ func main() { fmt.Println("Distributed under GPL.") fmt.Println() - inFile := flag.String("in", "", "input source file (required)") - outFile := flag.String("out", "", "output assembly file (required)") - flag.Parse() - - if *inFile == "" || *outFile == "" { - _, _ = fmt.Fprintln(os.Stderr, "Error: -in and -out are required") - flag.Usage() + // Check if we have any arguments + if len(os.Args) < 2 { + printUsage() os.Exit(1) } - if err := run(*inFile, *outFile); err != nil { - if _, ok := err.(preproc.HaltError); ok { - fmt.Println("Halted by #HALT directive") - os.Exit(2) + // Check for help flags + if os.Args[1] == "-h" || os.Args[1] == "--help" { + printUsage() + return + } + + // Check if first arg is a subcommand + subcommand := os.Args[1] + if subcommand == "build" || subcommand == "compile" || subcommand == "help" { + switch subcommand { + case "build": + runBuildCommand(os.Args[2:]) + case "compile": + runCompileCommand(os.Args[2:]) + case "help": + printUsage() } - _, _ = fmt.Fprintf(os.Stderr, "Error: %v\n", err) + return + } + + // Default mode: treat as build command with implicit arguments + // Parse arguments flexibly + var inputFile, outputFile string + args := os.Args[1:] + + for i := 0; i < len(args); i++ { + arg := args[i] + + if arg == "-i" || arg == "-in" { + if i+1 < len(args) { + inputFile = args[i+1] + i++ // Skip next arg + } + } else if arg == "-o" || arg == "-out" { + if i+1 < len(args) { + outputFile = args[i+1] + i++ // Skip next arg + } + } else if !strings.HasPrefix(arg, "-") && inputFile == "" { + // First non-flag argument is the input file + inputFile = arg + } + } + + if inputFile == "" { + fmt.Fprintln(os.Stderr, "Error: No input file specified") + printUsage() os.Exit(1) } - - fmt.Println("Compilation successful.") + + // Default output based on input + if outputFile == "" { + base := strings.TrimSuffix(filepath.Base(inputFile), filepath.Ext(inputFile)) + outputFile = base + ".prg" // Default to build mode (.prg) + } + + // Determine mode by output extension + if strings.HasSuffix(strings.ToLower(outputFile), ".prg") { + // Build mode (compile + assemble) + if err := build(inputFile, outputFile, false, false); err != nil { + handleError(err) + } + fmt.Println("Build successful.") + } else { + // Compile mode (assembly only) + if err := compileOnly(inputFile, outputFile); err != nil { + handleError(err) + } + fmt.Println("Compilation successful.") + } } -func run(inFile, outFile string) error { +func compileOnly(inFile, outFile string) error { // Preprocess lines, pragma, err := preproc.PreProcess(inFile) if err != nil { @@ -119,3 +177,177 @@ func registerCommands(comp *compiler.Compiler) { func writeOutput(filename string, lines []string) error { return os.WriteFile(filename, []byte(strings.Join(lines, "\n")+"\n"), 0644) } + +func handleError(err error) { + if _, ok := err.(preproc.HaltError); ok { + fmt.Println("Halted by #HALT directive") + os.Exit(2) + } + _, _ = fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) +} + +func printUsage() { + fmt.Println("Usage:") + fmt.Println(" c65gm build -i [-o ] [--keep-asm] [--no-cbm]") + fmt.Println(" c65gm compile -i [-o ]") + fmt.Println(" c65gm [-o ] (smart default mode)") + fmt.Println(" c65gm -in -out (legacy mode)") + fmt.Println() + fmt.Println("Commands:") + fmt.Println(" build Compile and assemble to .prg file (default)") + fmt.Println(" compile Compile to .asm file only") + fmt.Println(" help Show this help") + fmt.Println() + fmt.Println("Options:") + fmt.Println(" -i, --input Input .c65 file (required)") + fmt.Println(" -o, --output Output file (default: .prg for build, .asm for compile)") + fmt.Println(" --keep-asm Keep intermediate assembly file (build command only)") + fmt.Println(" --no-cbm Don't add -f cbm flag to ACME (build command only)") +} + +func runBuildCommand(args []string) { + buildCmd := flag.NewFlagSet("build", flag.ExitOnError) + input := buildCmd.String("i", "", "input .c65 file (required)") + output := buildCmd.String("o", "", "output .prg file (default: .prg)") + keepAsm := buildCmd.Bool("keep-asm", false, "keep intermediate assembly file") + noCBM := buildCmd.Bool("no-cbm", false, "don't add -f cbm flag to ACME") + + if err := buildCmd.Parse(args); err != nil { + fmt.Fprintf(os.Stderr, "Error parsing flags: %v\n", err) + os.Exit(1) + } + + if *input == "" { + fmt.Fprintln(os.Stderr, "Error: -i flag is required") + buildCmd.Usage() + os.Exit(1) + } + + // Default output filename + if *output == "" { + base := strings.TrimSuffix(filepath.Base(*input), filepath.Ext(*input)) + *output = base + ".prg" + } + + // Ensure output has .prg extension + if !strings.HasSuffix(strings.ToLower(*output), ".prg") { + *output = *output + ".prg" + } + + if err := build(*input, *output, *keepAsm, *noCBM); err != nil { + handleError(err) + } + fmt.Println("Build successful.") +} + +func runCompileCommand(args []string) { + compileCmd := flag.NewFlagSet("compile", flag.ExitOnError) + input := compileCmd.String("i", "", "input .c65 file (required)") + output := compileCmd.String("o", "", "output .asm file (default: .asm)") + + if err := compileCmd.Parse(args); err != nil { + fmt.Fprintf(os.Stderr, "Error parsing flags: %v\n", err) + os.Exit(1) + } + + if *input == "" { + fmt.Fprintln(os.Stderr, "Error: -i flag is required") + compileCmd.Usage() + os.Exit(1) + } + + // Default output filename + if *output == "" { + base := strings.TrimSuffix(filepath.Base(*input), filepath.Ext(*input)) + *output = base + ".asm" + } + + // Ensure output has .asm extension + if !strings.HasSuffix(strings.ToLower(*output), ".asm") { + *output = *output + ".asm" + } + + if err := compileOnly(*input, *output); err != nil { + handleError(err) + } + fmt.Println("Compilation successful.") +} + +func build(inputFile, outputFile string, keepAsm, noCBM bool) error { + // Check if ACME is available before starting compilation + if err := checkACMEAvailable(); err != nil { + return err + } + + // Create temporary assembly file + tempDir := os.TempDir() + asmFile := filepath.Join(tempDir, "c65gm_"+filepath.Base(inputFile)+".asm") + + // Compile to assembly + if err := compileOnly(inputFile, asmFile); err != nil { + return fmt.Errorf("compilation failed: %w", err) + } + + // Run ACME assembler + if err := runACME(asmFile, outputFile, noCBM); err != nil { + return fmt.Errorf("assembly failed: %w", err) + } + + // Clean up temporary assembly file unless requested to keep it + if !keepAsm { + os.Remove(asmFile) + } else { + fmt.Printf("Intermediate assembly file kept: %s\n", asmFile) + } + + return nil +} + +func runACME(asmFile, outputFile string, noCBM bool) error { + // Check if ACME is available + acmePath, err := exec.LookPath("acme") + if err != nil { + return fmt.Errorf("acme assembler not found in PATH. Please install ACME: https://github.com/meonwax/acme") + } + + // Build ACME command arguments + args := []string{"-o", outputFile} + if !noCBM { + args = append(args, "-f", "cbm") + } + args = append(args, asmFile) + + fmt.Printf("Running ACME assembler: %s %s\n", acmePath, strings.Join(args, " ")) + + cmd := exec.Command(acmePath, args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + return fmt.Errorf("acme failed: %w", err) + } + + fmt.Printf("Assembled to: %s\n", outputFile) + return nil +} + +// checkACMEAvailable checks if ACME is installed and provides helpful instructions if not +func checkACMEAvailable() error { + _, err := exec.LookPath("acme") + if err != nil { + return fmt.Errorf(`ACME assembler not found in PATH. + +c65gm requires ACME (Cross-Assembler) to create executable files. +Please install ACME from: https://github.com/meonwax/acme + +Installation options: +- Linux: Download from releases or use package manager +- macOS: brew install acme +- Windows: Download binary from releases + +Or use compile mode to generate assembly only: + c65gm compile -i program.c65`) + } + return nil +}