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
.local/
opencode-config/package-lock.json
internal/preproc/lib/
.bun/

View file

@ -90,11 +90,13 @@ LABEL lib_mylib_skip
## Development Guidelines
### 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
2. Make code changes
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
**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.)
@ -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
### 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:
- 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
PROGNAME="shift_demo"
# Only set C65LIBPATH if not already defined
if [ -z "$C65LIBPATH" ]; then
export C65LIBPATH=$(readlink -f "../../lib")
fi
#if [ -z "$C65LIBPATH" ]; then
# export C65LIBPATH=$(readlink -f "../../lib")
#fi
# Compile - use absolute path to c65gm
c65gm -in ${PROGNAME}.c65 -out ${PROGNAME}.s
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
}
// 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 (
"os"
"path/filepath"
"strings"
"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) {
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 {
r = reader[0]
} else {
r = NewDiskFileReader()
r = NewDefaultFileReader()
}
pp := newPreproc(r)
lines, err := pp.run(rootFilename)