Skip to content

Commit fcfede2

Browse files
committed
Add HTTP server, Go native packages, hash verification, bump to v1.1.0
- http.listen with VM callback for serving HTTP from CColon - Go native package loading via plugin system (Linux/macOS) - SHA256 hash verification in install scripts - Platform-specific console terminal size (fixes Windows build) - Updated docs: sint in variables/methods reference, package loading details
1 parent d36f40e commit fcfede2

18 files changed

Lines changed: 588 additions & 52 deletions

File tree

.github/workflows/release.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ jobs:
7171
path: artifacts
7272
merge-multiple: true
7373

74+
- name: Generate checksums
75+
run: |
76+
cd artifacts
77+
sha256sum * > checksums.txt
78+
cat checksums.txt
79+
7480
- name: Create or update release
7581
env:
7682
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

ccolon/main.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import (
1717
"github.com/TRC-Loop/ccolon/vm"
1818
)
1919

20-
const version = "1.0.0"
20+
const version = "1.1.0"
2121

2222
func main() {
2323
if len(os.Args) < 2 {
@@ -324,6 +324,9 @@ func runFile(source string, baseDir string) error {
324324
return compileSource(string(data))
325325
}
326326

327+
// Load installed packages
328+
pkg.LoadPackages(machine, compileSource)
329+
327330
if err := machine.Run(fn); err != nil {
328331
return err
329332
}

ccolon/pkg/loader.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package pkg
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
9+
"github.com/TRC-Loop/ccolon/compiler"
10+
"github.com/TRC-Loop/ccolon/vm"
11+
)
12+
13+
// LoadPackages scans ~/.ccolon/packages/ and loads all installed packages
14+
// into the VM. CColon packages are registered as file import sources.
15+
// Go native packages are compiled and loaded as plugins.
16+
func LoadPackages(machine *vm.VM, compileSource func(string) (*compiler.FuncObject, error)) error {
17+
pkgDir, err := packagesDir()
18+
if err != nil {
19+
return nil // no packages dir, nothing to load
20+
}
21+
22+
entries, err := os.ReadDir(pkgDir)
23+
if err != nil {
24+
return nil
25+
}
26+
27+
for _, e := range entries {
28+
if !e.IsDir() {
29+
continue
30+
}
31+
dir := filepath.Join(pkgDir, e.Name())
32+
manifestPath := filepath.Join(dir, "ccolon.json")
33+
data, err := os.ReadFile(manifestPath)
34+
if err != nil {
35+
continue // skip packages without manifest
36+
}
37+
38+
var m Manifest
39+
if err := json.Unmarshal(data, &m); err != nil {
40+
continue
41+
}
42+
43+
switch m.Type {
44+
case "go":
45+
if err := LoadGoPlugin(dir, machine); err != nil {
46+
fmt.Fprintf(os.Stderr, "warning: failed to load Go package '%s': %s\n", m.Name, err)
47+
}
48+
default:
49+
// CCL package: register as an importable module by running the entry file
50+
entry := m.Entry
51+
if entry == "" {
52+
entry = "lib.ccl"
53+
}
54+
entryPath := filepath.Join(dir, entry)
55+
source, err := os.ReadFile(entryPath)
56+
if err != nil {
57+
continue // skip if entry file doesn't exist
58+
}
59+
fn, err := compileSource(string(source))
60+
if err != nil {
61+
fmt.Fprintf(os.Stderr, "warning: failed to compile package '%s': %s\n", m.Name, err)
62+
continue
63+
}
64+
// Run the package code to register its globals/functions
65+
if err := machine.Run(fn); err != nil {
66+
fmt.Fprintf(os.Stderr, "warning: failed to load package '%s': %s\n", m.Name, err)
67+
}
68+
}
69+
}
70+
return nil
71+
}

ccolon/pkg/manifest.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ type Manifest struct {
1616
Description string `json:"description,omitempty"`
1717
Dependencies map[string]string `json:"dependencies,omitempty"`
1818
Registry string `json:"registry,omitempty"`
19+
Type string `json:"type,omitempty"` // "ccl" (default) or "go"
20+
Entry string `json:"entry,omitempty"` // entry point file
1921
}
2022

2123
// RegistryURL returns the effective registry URL, checking the manifest field

ccolon/pkg/plugin_unix.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
//go:build linux || darwin
2+
3+
package pkg
4+
5+
import (
6+
"fmt"
7+
"os/exec"
8+
"path/filepath"
9+
"plugin"
10+
11+
"github.com/TRC-Loop/ccolon/vm"
12+
)
13+
14+
// LoadGoPlugin compiles and loads a Go native package.
15+
// The package directory must contain Go source files with a Register function.
16+
func LoadGoPlugin(pkgDir string, machine *vm.VM) error {
17+
soPath := filepath.Join(pkgDir, "plugin.so")
18+
19+
// Compile the plugin
20+
cmd := exec.Command("go", "build", "-buildmode=plugin", "-o", soPath, ".")
21+
cmd.Dir = pkgDir
22+
output, err := cmd.CombinedOutput()
23+
if err != nil {
24+
return fmt.Errorf("failed to compile Go plugin in %s: %s\n%s", pkgDir, err, string(output))
25+
}
26+
27+
// Load the plugin
28+
p, err := plugin.Open(soPath)
29+
if err != nil {
30+
return fmt.Errorf("failed to load plugin %s: %s", soPath, err)
31+
}
32+
33+
// Look up the Register function
34+
sym, err := p.Lookup("Register")
35+
if err != nil {
36+
return fmt.Errorf("plugin %s has no Register function: %s", pkgDir, err)
37+
}
38+
39+
registerFn, ok := sym.(func(*vm.VM))
40+
if !ok {
41+
return fmt.Errorf("plugin %s Register function has wrong signature (expected func(*vm.VM))", pkgDir)
42+
}
43+
44+
registerFn(machine)
45+
return nil
46+
}

ccolon/pkg/plugin_windows.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
//go:build windows
2+
3+
package pkg
4+
5+
import (
6+
"fmt"
7+
8+
"github.com/TRC-Loop/ccolon/vm"
9+
)
10+
11+
// LoadGoPlugin is not supported on Windows.
12+
func LoadGoPlugin(pkgDir string, machine *vm.VM) error {
13+
return fmt.Errorf("Go native plugins are not supported on Windows")
14+
}

ccolon/stdlib/console.go

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ import (
55
"fmt"
66
"os"
77
"strings"
8-
"syscall"
9-
"unsafe"
108

119
"github.com/TRC-Loop/ccolon/vm"
1210
)
@@ -108,28 +106,11 @@ func NewConsoleModule() *vm.ModuleValue {
108106
"getSize": {
109107
Name: "console.getSize",
110108
Fn: func(args []vm.Value) (vm.Value, error) {
111-
type winsize struct {
112-
Row uint16
113-
Col uint16
114-
Xpixel uint16
115-
Ypixel uint16
116-
}
117-
ws := &winsize{}
118-
_, _, err := syscall.Syscall(syscall.SYS_IOCTL,
119-
uintptr(syscall.Stdout),
120-
uintptr(syscall.TIOCGWINSZ),
121-
uintptr(unsafe.Pointer(ws)))
122-
123-
width := int64(80)
124-
height := int64(24)
125-
if err == 0 {
126-
width = int64(ws.Col)
127-
height = int64(ws.Row)
128-
}
109+
w, h := getTerminalSize()
129110
return &vm.DictValue{
130111
Entries: map[string]vm.Value{
131-
"width": &vm.IntValue{Val: width},
132-
"height": &vm.IntValue{Val: height},
112+
"width": &vm.IntValue{Val: int64(w)},
113+
"height": &vm.IntValue{Val: int64(h)},
133114
},
134115
Order: []string{"width", "height"},
135116
}, nil

ccolon/stdlib/console_size_unix.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
//go:build !windows
2+
3+
package stdlib
4+
5+
import (
6+
"syscall"
7+
"unsafe"
8+
)
9+
10+
func getTerminalSize() (width, height int) {
11+
type winsize struct {
12+
Row uint16
13+
Col uint16
14+
Xpixel uint16
15+
Ypixel uint16
16+
}
17+
ws := &winsize{}
18+
_, _, err := syscall.Syscall(syscall.SYS_IOCTL,
19+
uintptr(syscall.Stdout),
20+
uintptr(syscall.TIOCGWINSZ),
21+
uintptr(unsafe.Pointer(ws)))
22+
23+
if err != 0 {
24+
return 80, 24
25+
}
26+
return int(ws.Col), int(ws.Row)
27+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
//go:build windows
2+
3+
package stdlib
4+
5+
import (
6+
"os"
7+
"os/exec"
8+
"strconv"
9+
"strings"
10+
)
11+
12+
func getTerminalSize() (width, height int) {
13+
cmd := exec.Command("cmd", "/c", "mode", "con")
14+
cmd.Stdin = os.Stdin
15+
out, err := cmd.Output()
16+
if err != nil {
17+
return 80, 24
18+
}
19+
lines := strings.Split(string(out), "\n")
20+
w, h := 80, 24
21+
for _, line := range lines {
22+
line = strings.TrimSpace(line)
23+
if strings.Contains(line, "Columns") || strings.Contains(line, "Spalten") {
24+
parts := strings.Fields(line)
25+
if len(parts) > 0 {
26+
if n, err := strconv.Atoi(parts[len(parts)-1]); err == nil {
27+
w = n
28+
}
29+
}
30+
}
31+
if strings.Contains(line, "Lines") || strings.Contains(line, "Zeilen") {
32+
parts := strings.Fields(line)
33+
if len(parts) > 0 {
34+
if n, err := strconv.Atoi(parts[len(parts)-1]); err == nil {
35+
h = n
36+
}
37+
}
38+
}
39+
}
40+
return w, h
41+
}

ccolon/stdlib/http.go

Lines changed: 82 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
"github.com/TRC-Loop/ccolon/vm"
1010
)
1111

12-
func NewHttpModule() *vm.ModuleValue {
12+
func NewHttpModule(machine *vm.VM) *vm.ModuleValue {
1313
return &vm.ModuleValue{
1414
Name: "http",
1515
Methods: map[string]*vm.NativeFuncValue{
@@ -43,12 +43,89 @@ func NewHttpModule() *vm.ModuleValue {
4343
if !ok {
4444
return nil, fmt.Errorf("http.listen() handler must be a function")
4545
}
46-
_ = handler // handler will be called via VM callback
46+
4747
addr := fmt.Sprintf(":%d", port.Val)
4848
fmt.Printf("CColon HTTP server listening on %s\n", addr)
49-
// We store the handler ref but can't call it without VM access.
50-
// The server integration is done through the VM's CallFunc.
51-
return nil, fmt.Errorf("http.listen() requires VM callback support (use the listen helper)")
49+
50+
mux := http.NewServeMux()
51+
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
52+
body, _ := io.ReadAll(r.Body)
53+
r.Body.Close()
54+
55+
// Build request dict
56+
headerDict := &vm.DictValue{
57+
Entries: make(map[string]vm.Value),
58+
Order: []string{},
59+
}
60+
for key := range r.Header {
61+
lk := strings.ToLower(key)
62+
headerDict.Entries[lk] = &vm.StringValue{Val: r.Header.Get(key)}
63+
headerDict.Order = append(headerDict.Order, lk)
64+
}
65+
66+
reqDict := &vm.DictValue{
67+
Entries: map[string]vm.Value{
68+
"method": &vm.StringValue{Val: r.Method},
69+
"path": &vm.StringValue{Val: r.URL.Path},
70+
"body": &vm.StringValue{Val: string(body)},
71+
"headers": headerDict,
72+
"query": &vm.StringValue{Val: r.URL.RawQuery},
73+
},
74+
Order: []string{"method", "path", "body", "headers", "query"},
75+
}
76+
77+
result, err := machine.CallFunc(handler, []vm.Value{reqDict})
78+
if err != nil {
79+
http.Error(w, "handler error: "+err.Error(), 500)
80+
return
81+
}
82+
83+
// Handle response
84+
switch resp := result.(type) {
85+
case *vm.StringValue:
86+
w.Header().Set("Content-Type", "text/plain")
87+
w.WriteHeader(200)
88+
w.Write([]byte(resp.Val))
89+
case *vm.DictValue:
90+
// Expect {status: int, body: string, headers: dict}
91+
status := 200
92+
respBody := ""
93+
if s, ok := resp.Entries["status"]; ok {
94+
if iv, ok := s.(*vm.IntValue); ok {
95+
status = int(iv.Val)
96+
}
97+
}
98+
if b, ok := resp.Entries["body"]; ok {
99+
if sv, ok := b.(*vm.StringValue); ok {
100+
respBody = sv.Val
101+
}
102+
}
103+
if h, ok := resp.Entries["headers"]; ok {
104+
if hd, ok := h.(*vm.DictValue); ok {
105+
for key, val := range hd.Entries {
106+
if sv, ok := val.(*vm.StringValue); ok {
107+
w.Header().Set(key, sv.Val)
108+
}
109+
}
110+
}
111+
}
112+
w.WriteHeader(status)
113+
w.Write([]byte(respBody))
114+
case *vm.NilValue:
115+
w.WriteHeader(204)
116+
default:
117+
w.Header().Set("Content-Type", "text/plain")
118+
w.WriteHeader(200)
119+
w.Write([]byte(result.String()))
120+
}
121+
})
122+
123+
// This blocks, so it won't return until the server stops
124+
err := http.ListenAndServe(addr, mux)
125+
if err != nil {
126+
return nil, fmt.Errorf("http.listen() failed: %s", err)
127+
}
128+
return &vm.NilValue{}, nil
52129
},
53130
},
54131
},

0 commit comments

Comments
 (0)