From 027eda50696d6ba7f732b5c3496ac0e3071b292c Mon Sep 17 00:00:00 2001 From: Mattias Hansson Date: Fri, 17 Apr 2026 16:02:44 +0200 Subject: [PATCH] Added help for users to get acme installed on their system if it's missing. --- README.md | 141 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- main.go | 122 +++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 245 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index b8248f3..cda2058 100644 --- a/README.md +++ b/README.md @@ -16,10 +16,23 @@ c65gm compiles high-level source code into ACME assembler syntax for the 6502 pr - **Optimizations**: Constant folding, self-assignment detection - **Safety features**: Compile-time detection of overlapping absolute addresses in function call chains -## Requirements +## Prerequisites +### ACME Assembler (Required for creating .prg executables) +c65gm requires the ACME Cross-Assembler to create executable .prg files. If you only need to generate assembly (.asm) files, you can use compile mode without ACME. + +**Installation options by platform:** + +- **Ubuntu/Debian**: `sudo apt install acme` +- **Fedora/RHEL**: `sudo dnf install acme` +- **Arch Linux**: `sudo pacman -S acme` +- **macOS**: `brew install acme` +- **Windows**: Download binary from [ACME releases](https://github.com/meonwax/acme/releases) + +If ACME is not found when running the `build` command, c65gm will display platform-specific installation instructions. + +### Go (Required for building c65gm from source) - **Go**: Version 1.25.1 or higher (tested with 1.25.5) -- **Acme**: Tested with release 0.97 ("Zem") The project uses Go modules with these dependencies: @@ -75,13 +88,116 @@ Compile and assemble directly to a .prg file: ``` ### Key Features -- **Self-contained**: No external build scripts needed +- **Self-contained**: Embedded standard library and Starlark interpreter (ACME assembler required for .prg creation) - **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 +- **ACME integration**: Automatically finds and runs ACME assembler with `-f cbm` by default (provides platform-specific installation instructions if missing) - **Backward compatible**: Legacy `-in`/`-out` flags still work - **Customizable**: Use `--no-cbm` to disable CBM format, `--keep-asm` to keep intermediate files +## Compiler Options Reference + +### Default Behavior (Simplest Case) +The simplest way to use c65gm is to provide just the input file: +```bash +c65gm myprogram.c65 +``` +This will: +1. Compile `myprogram.c65` to assembly +2. Run ACME assembler with `-f cbm` format +3. Produce `myprogram.prg` (C64 executable) + +### Input and Output Files +- **`-i`, `--input`** (`-in`): Specify input `.c65` file (required) +- **`-o`, `--output`** (`-out`): Specify output file (optional) + +**Examples:** +```bash +# Explicit input/output +c65gm -i program.c65 -o game.prg + +# Input only (default output: program.prg) +c65gm program.c65 + +# Legacy syntax (still works) +c65gm -in program.c65 -out output.asm +``` + +### Operation Modes +c65gm automatically determines the operation mode based on the output file extension: + +#### Build Mode (`.prg` extension) +Compiles **and** assembles to create a C64 executable: +```bash +c65gm program.c65 # → program.prg (default) +c65gm program.c65 -o game.prg # → game.prg +``` + +#### Compile Mode (`.asm` extension) +Compiles only, producing ACME assembly for manual assembly: +```bash +c65gm program.c65 -o output.asm # → output.asm +c65gm compile -i program.c65 # → program.asm +``` + +### Subcommands +For explicit control, use subcommands: + +#### `build` - Compile + Assemble +```bash +c65gm build -i program.c65 [-o output.prg] [--keep-asm] [--no-cbm] +``` +- `--keep-asm`: Keep intermediate `.asm` file +- `--no-cbm`: Don't add `-f cbm` flag to ACME (for non-CBM targets) + +#### `compile` - Compile Only +```bash +c65gm compile -i program.c65 [-o output.asm] +``` +Produces assembly file only, doesn't run ACME. + +#### `help` - Show Usage +```bash +c65gm help +c65gm -h +c65gm --help +``` + +### ACME Assembler Integration +c65gm automatically: +1. Searches `PATH` for `acme` executable +2. Runs: `acme -o output.prg -f cbm temp.asm` +3. Shows ACME output and errors +4. Cleans up temporary files (unless `--keep-asm`) + +**Error Handling:** If ACME is not found, c65gm shows installation instructions and suggests using `compile` mode instead. + +### Environment Variables +- **`C65LIBPATH`**: Search path for `#INCLUDE ` directives + ```bash + export C65LIBPATH=/path/to/c65gm/lib + c65gm program.c65 + ``` + +### Examples Summary +```bash +# Quick build (recommended) +c65gm program.c65 + +# Custom output name +c65gm -i program.c65 -o game.prg + +# Keep intermediate assembly +c65gm build -i program.c65 --keep-asm + +# Compile only (no ACME) +c65gm compile -i program.c65 + +# Legacy two-step process (still works) +c65gm -in program.c65 -out program.asm +acme -f cbm -o program.prg program.asm +``` + ## Running Tests Run all tests: @@ -135,4 +251,19 @@ The example directories also contain `cm.sh` scripts showing the old build metho ## License Copyright (C) 1999, 2025 Mattias Hansson -Distributed under GPL. + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +See [LICENSE](LICENSE) file for the full GPL v2 license text. diff --git a/main.go b/main.go index 3e6856b..1072107 100644 --- a/main.go +++ b/main.go @@ -1,12 +1,15 @@ package main import ( + "context" "flag" "fmt" "os" "os/exec" "path/filepath" + "runtime" "strings" + "time" "c65gm/internal/commands" "c65gm/internal/compiler" @@ -17,6 +20,13 @@ import ( // Copyright (C) 1999, 2025 Mattias Hansson // Distributed under GPL. +// ANSI color codes for error messages +const ( + colorRed = "\033[31m" + colorYellow = "\033[33m" + colorReset = "\033[0m" +) + func main() { fmt.Println("c65gm - A 6502 Cross-Compiler for the ACME Cross-Assembler.") fmt.Println("Copyright (C) 1999, 2025 Mattias Hansson. v1.0.0") @@ -61,11 +71,19 @@ func main() { if i+1 < len(args) { inputFile = args[i+1] i++ // Skip next arg + } else { + fmt.Fprintln(os.Stderr, "Error: -i flag requires a filename") + printUsage() + os.Exit(1) } } else if arg == "-o" || arg == "-out" { if i+1 < len(args) { outputFile = args[i+1] i++ // Skip next arg + } else { + fmt.Fprintln(os.Stderr, "Error: -o flag requires a filename") + printUsage() + os.Exit(1) } } else if !strings.HasPrefix(arg, "-") && inputFile == "" { // First non-flag argument is the input file @@ -308,7 +326,7 @@ 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") + return fmt.Errorf("acme assembler not found in PATH") } // Build ACME command arguments @@ -332,22 +350,100 @@ func runACME(asmFile, outputFile string, noCBM bool) error { return nil } +// detectPackageManager detects available package managers and returns platform-specific installation instructions +func detectPackageManager() (string, string) { + // Platform detection + os := runtime.GOOS + + // Helper function to check if a command exists + commandExists := func(cmd string) bool { + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) + defer cancel() + + // Try "command -v" first (POSIX standard) + checkCmd := exec.CommandContext(ctx, "command", "-v", cmd) + if err := checkCmd.Run(); err == nil { + return true + } + + // Fallback to "which" (common but not POSIX) + checkCmd = exec.CommandContext(ctx, "which", cmd) + if err := checkCmd.Run(); err == nil { + return true + } + + return false + } + + switch os { + case "linux": + // Linux package manager detection (priority order) + if commandExists("apt") { + return "Ubuntu/Debian (apt)", "sudo apt install acme" + } + if commandExists("dnf") { + return "Fedora/RHEL (dnf)", "sudo dnf install acme" + } + if commandExists("yum") { + return "RHEL/CentOS (yum)", "sudo yum install acme" + } + if commandExists("pacman") { + return "Arch (pacman)", "sudo pacman -S acme" + } + if commandExists("zypper") { + return "openSUSE (zypper)", "sudo zypper install acme" + } + // Generic Linux fallback + return "Linux", "Download from releases or use your package manager" + + case "darwin": // macOS + if commandExists("brew") { + return "macOS (Homebrew)", "brew install acme" + } + return "macOS", "brew install acme # Install Homebrew first, or download binary from releases" + + case "windows": + // Windows package manager detection + if commandExists("choco") { + return "Windows (Chocolatey)", "choco install acme" + } + if commandExists("scoop") { + return "Windows (Scoop)", "scoop install acme" + } + // Check for winget (Windows Package Manager) + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) + defer cancel() + checkCmd := exec.CommandContext(ctx, "cmd", "/c", "where", "winget") + if err := checkCmd.Run(); err == nil { + return "Windows (winget)", "winget install acme" + } + return "Windows", "Download binary from: https://github.com/meonwax/acme/releases" + + default: + return "Unknown OS", "Download from: https://github.com/meonwax/acme" + } +} + // 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`) + platform, installCmd := detectPackageManager() + + var message strings.Builder + message.WriteString(fmt.Sprintf("%sACME assembler not found in PATH%s\n\n", colorRed, colorReset)) + message.WriteString("c65gm requires ACME (Cross-Assembler) to create executable files.\n\n") + + message.WriteString(fmt.Sprintf("%sInstallation options for %s:%s\n", colorYellow, platform, colorReset)) + message.WriteString(fmt.Sprintf(" %s\n\n", installCmd)) + + message.WriteString("Or use compile mode to generate assembly only:\n") + message.WriteString(" c65gm compile -i program.c65\n\n") + + message.WriteString("For manual download or more information:\n") + message.WriteString(" https://github.com/meonwax/acme") + + return fmt.Errorf("%s", message.String()) } return nil }