Added c65cm stdlib as embedded into the c65gm exe so it's completely self contained, for easy distribution.

This commit is contained in:
Mattias Hansson 2026-04-17 11:53:20 +02:00
parent 5e71adff2b
commit 59f056734f
9 changed files with 194 additions and 6 deletions

2
.gitignore vendored
View file

@ -38,3 +38,5 @@ c65gm
.env .env
.local/ .local/
opencode-config/package-lock.json opencode-config/package-lock.json
internal/preproc/lib/
.bun/

View file

@ -90,11 +90,13 @@ LABEL lib_mylib_skip
## Development Guidelines ## Development Guidelines
### Environment Constraints ### Environment Constraints
**IMPORTANT**: The agent runs in a Docker container without access to compilers, testing tools, or external build systems. All compilation and testing must be performed by the user. The agent can only: **CRITICAL - NO GO TOOLS AVAILABLE**: The agent runs in a Docker container without access to compilers, testing tools, or external build systems. The container does NOT have Go installed (`go` command not found). All compilation and testing must be performed by the user. The agent can only:
1. Read and analyze source code 1. Read and analyze source code
2. Make code changes 2. Make code changes
3. Provide instructions for the user to compile and test 3. Provide instructions for the user to compile and test
**NEVER attempt to run `go build`, `go test`, or any Go commands** - they will fail with "go: not found".
### File Access Restrictions ### File Access Restrictions
**CRITICAL**: The agent must only access normal project files within the current working directory. The agent must NEVER: **CRITICAL**: The agent must only access normal project files within the current working directory. The agent must NEVER:
1. Look at files outside the project directory (e.g., `/tmp/`, `/etc/`, `/home/`, etc.) 1. Look at files outside the project directory (e.g., `/tmp/`, `/etc/`, `/home/`, etc.)
@ -117,7 +119,7 @@ All file operations must be restricted to the project's source code and document
4. Consider adding examples in `examples/` directory 4. Consider adding examples in `examples/` directory
### Testing and Rebuilding ### Testing and Rebuilding
**IMPORTANT**: The agent cannot run tests or compile code. Provide these instructions to the user: **CRITICAL**: The agent cannot run tests or compile code. The container has no Go installation. Provide these instructions to the user:
#### Testing: #### Testing:
- Run all tests: `go test ./...` - Run all tests: `go test ./...`

13
build_c65gm.sh Executable file
View file

@ -0,0 +1,13 @@
#!/bin/bash
set -e
# Copy lib directory to internal/preproc for embedding
echo "Copying lib directory for embedding..."
rm -rf internal/preproc/lib
cp -r lib internal/preproc/
# Build the binary
echo "Building c65gm..."
go build -o c65gm
echo "Build complete: c65gm"

View file

@ -2,9 +2,9 @@
# Define filename as variable # Define filename as variable
PROGNAME="shift_demo" PROGNAME="shift_demo"
# Only set C65LIBPATH if not already defined # Only set C65LIBPATH if not already defined
if [ -z "$C65LIBPATH" ]; then #if [ -z "$C65LIBPATH" ]; then
export C65LIBPATH=$(readlink -f "../../lib") # export C65LIBPATH=$(readlink -f "../../lib")
fi #fi
# Compile - use absolute path to c65gm # Compile - use absolute path to c65gm
c65gm -in ${PROGNAME}.c65 -out ${PROGNAME}.s c65gm -in ${PROGNAME}.c65 -out ${PROGNAME}.s
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then

View file

@ -0,0 +1,38 @@
package preproc
import (
"embed"
"fmt"
"io/fs"
"strings"
)
//go:embed lib
var embeddedLib embed.FS
// embeddedLibPath returns the path within the embedded filesystem for a library include.
// For example, "<c64start.c65>" becomes "lib/c64start.c65"
func embeddedLibPath(spec string) (string, error) {
if !strings.HasPrefix(spec, "<") || !strings.HasSuffix(spec, ">") {
return "", fmt.Errorf("not an angle-bracket include: %s", spec)
}
base := strings.TrimSpace(spec[1 : len(spec)-1])
return "lib/" + base, nil
}
// readEmbeddedFile reads a file from the embedded filesystem.
func readEmbeddedFile(path string) ([]string, error) {
data, err := fs.ReadFile(embeddedLib, path)
if err != nil {
return nil, err
}
// Split into lines, preserving empty lines
content := string(data)
lines := strings.Split(content, "\n")
// If the file ends with newline, the last element will be empty
// This matches the behavior of strings.Split for disk files
return lines, nil
}

View file

@ -93,3 +93,16 @@ func (m *MockFileReader) ReadLines(includeSpec string, _ string) ([]string, stri
} }
return lines, includeSpec, nil return lines, includeSpec, nil
} }
// NewDefaultFileReader creates the appropriate FileReader based on C65LIBPATH environment variable.
// If C65LIBPATH is set, returns a DiskFileReader for external library access.
// If C65LIBPATH is not set, returns a HybridFileReader that uses embedded standard library.
func NewDefaultFileReader() FileReader {
libPath := os.Getenv("C65LIBPATH")
if libPath != "" {
fmt.Printf("Using external library from: %s\n", libPath)
return NewDiskFileReader()
}
fmt.Println("Using embedded standard library")
return NewHybridFileReader()
}

View file

@ -3,6 +3,7 @@ package preproc
import ( import (
"os" "os"
"path/filepath" "path/filepath"
"strings"
"testing" "testing"
) )
@ -67,6 +68,51 @@ func TestDiskFileReader_ReadLines_LibraryWithoutEnv(t *testing.T) {
} }
} }
func TestHybridFileReader_ReadLines_LibraryIncludes(t *testing.T) {
origPath := os.Getenv("C65LIBPATH")
defer func() { _ = os.Setenv("C65LIBPATH", origPath) }()
// Test 1: With C65LIBPATH set, should use disk
libPath, _ := filepath.Abs("filereader_mocks/lib")
_ = os.Setenv("C65LIBPATH", libPath)
reader1 := NewHybridFileReader()
lines1, path1, err1 := reader1.ReadLines("<test_lib.c65>", "")
if err1 != nil {
t.Errorf("HybridFileReader with C65LIBPATH should work: %v", err1)
}
if len(lines1) == 0 {
t.Error("expected lines from disk library")
}
if !strings.Contains(path1, "filereader_mocks") {
t.Errorf("expected disk path, got: %s", path1)
}
// Test 2: Without C65LIBPATH, should use embedded (will fail in test since no embedded mock)
_ = os.Setenv("C65LIBPATH", "")
reader2 := NewHybridFileReader()
_, _, err2 := reader2.ReadLines("<test_lib.c65>", "")
if err2 == nil {
// This is expected to fail in unit tests since we can't mock embedded FS easily
// In real usage, embedded library would be used
t.Log("Note: HybridFileReader without C65LIBPATH would use embedded library")
}
}
func TestHybridFileReader_ReadLines_RelativeIncludes(t *testing.T) {
reader := NewHybridFileReader()
appDir, _ := filepath.Abs("filereader_mocks/app")
lines, _, err := reader.ReadLines("test_app.c65", appDir)
if err != nil {
t.Errorf("HybridFileReader should handle relative includes: %v", err)
}
if len(lines) == 0 {
t.Error("expected lines from relative include")
}
}
func TestDiskFileReader_ReadLines_RelativeIncludes(t *testing.T) { func TestDiskFileReader_ReadLines_RelativeIncludes(t *testing.T) {
reader := NewDiskFileReader() reader := NewDiskFileReader()

View file

@ -0,0 +1,74 @@
package preproc
import (
"fmt"
"os"
"strings"
)
// HybridFileReader implements FileReader with support for both embedded
// standard library (when C65LIBPATH is not set) and disk-based includes.
type HybridFileReader struct {
diskReader *DiskFileReader // For relative includes & C65LIBPATH overrides
libPath string // From C65LIBPATH env var
usedEmbedded bool // Track if we've used embedded library
embeddedCache map[string][]string // Cache for embedded files
}
// NewHybridFileReader creates a new HybridFileReader.
func NewHybridFileReader() *HybridFileReader {
return &HybridFileReader{
diskReader: NewDiskFileReader(),
libPath: os.Getenv("C65LIBPATH"),
embeddedCache: make(map[string][]string),
}
}
// ReadLines implements the FileReader interface.
func (h *HybridFileReader) ReadLines(includeSpec string, currentDir string) ([]string, string, error) {
// Check if this is an angle-bracket include
if strings.HasPrefix(includeSpec, "<") && strings.HasSuffix(includeSpec, ">") {
// If C65LIBPATH is set, delegate to DiskFileReader
if h.libPath != "" {
return h.diskReader.ReadLines(includeSpec, currentDir)
}
// Otherwise, use embedded library
if !h.usedEmbedded {
h.usedEmbedded = true
// Note: Library source message is printed by NewDefaultFileReader factory
}
return h.readEmbeddedLibrary(includeSpec)
}
// Relative includes always go to disk
return h.diskReader.ReadLines(includeSpec, currentDir)
}
// readEmbeddedLibrary reads a file from the embedded standard library.
func (h *HybridFileReader) readEmbeddedLibrary(spec string) ([]string, string, error) {
// Convert angle-bracket spec to embedded path
embeddedPath, err := embeddedLibPath(spec)
if err != nil {
return nil, "", err
}
// Check cache
if lines, ok := h.embeddedCache[embeddedPath]; ok {
// Return cached lines with virtual path for error messages
return lines, "<embedded>/" + strings.TrimPrefix(embeddedPath, "lib/"), nil
}
// Read from embedded filesystem
lines, err := readEmbeddedFile(embeddedPath)
if err != nil {
return nil, "", fmt.Errorf("embedded library include not found: %s", spec)
}
// Cache the result
h.embeddedCache[embeddedPath] = lines
// Return with virtual path for error messages
return lines, "<embedded>/" + strings.TrimPrefix(embeddedPath, "lib/"), nil
}

View file

@ -38,7 +38,7 @@ func PreProcess(rootFilename string, reader ...FileReader) ([]Line, *Pragma, err
if len(reader) > 0 && reader[0] != nil { if len(reader) > 0 && reader[0] != nil {
r = reader[0] r = reader[0]
} else { } else {
r = NewDiskFileReader() r = NewDefaultFileReader()
} }
pp := newPreproc(r) pp := newPreproc(r)
lines, err := pp.run(rootFilename) lines, err := pp.run(rootFilename)