sorcery-go/pkg/quill/quill.go

156 lines
4.8 KiB
Go
Executable File

// Package quill is the "Scribe" of the Coven — the interactive wizard that
// interviews a developer and emits a new spell (DETAILS, DEPENDS, BUILD,
// CONFIGURE) into the Grimoire.
//
// Replacing the original Bash Quill, this Go version uses text/template
// for type-safe file generation and an HTTP-friendly SpellData struct so
// the WebUI can drive the same interview through a modal form.
package quill
import (
"crypto/sha512"
"encoding/hex"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"text/template"
"time"
)
// SpellData is everything the interview collects.
type SpellData struct {
Name string
Version string
SourceURL string
Hash string
Website string
License string
Description string
LongDesc string
Dependencies []string
Date string
}
// GenerateSpell writes a new spell directory under `targetDir` containing
// DETAILS, DEPENDS, BUILD and CONFIGURE. Atomic "buffer-then-commit": no
// file is written until the template parses successfully.
func GenerateSpell(data SpellData, targetDir string) error {
if data.Date == "" {
data.Date = time.Now().Format("20060102")
}
if data.Hash == "" {
// "Smart Quill" — auto-hash the upstream source.
h, err := autoHash(data.SourceURL)
if err == nil {
data.Hash = h
}
}
tmpl, err := template.New("details").Parse(detailsTmpl)
if err != nil {
return fmt.Errorf("quill: template parse: %w", err)
}
if err := os.MkdirAll(targetDir, 0755); err != nil {
return err
}
f, err := os.Create(filepath.Join(targetDir, "DETAILS"))
if err != nil {
return err
}
defer f.Close()
if err := tmpl.Execute(f, data); err != nil {
return fmt.Errorf("quill: template exec: %w", err)
}
// DEPENDS
depTmpl, err := template.New("depends").Parse(dependsTmpl)
if err != nil {
return fmt.Errorf("quill: parse depends template: %w", err)
}
df, err := os.Create(filepath.Join(targetDir, "DEPENDS"))
if err != nil {
return fmt.Errorf("quill: create DEPENDS: %w", err)
}
defer df.Close()
if err := depTmpl.Execute(df, data); err != nil {
return fmt.Errorf("quill: exec depends template: %w", err)
}
// BUILD
bf, err := os.Create(filepath.Join(targetDir, "BUILD"))
if err != nil {
return fmt.Errorf("quill: create BUILD: %w", err)
}
defer bf.Close()
if _, err := bf.WriteString(buildTmpl); err != nil {
return fmt.Errorf("quill: write BUILD: %w", err)
}
// CONFIGURE
cf, err := os.Create(filepath.Join(targetDir, "CONFIGURE"))
if err != nil {
return fmt.Errorf("quill: create CONFIGURE: %w", err)
}
defer cf.Close()
if _, err := cf.WriteString(configureTmpl); err != nil {
return fmt.Errorf("quill: write CONFIGURE: %w", err)
}
return nil
}
// autoHash downloads the source URL and computes its SHA-512 — Quill's
// "Smart" mode that saves the developer from running sha512sum by hand.
func autoHash(url string) (string, error) {
if url == "" {
return "", fmt.Errorf("quill: no source URL")
}
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("quill: HTTP %d fetching %s", resp.StatusCode, url)
}
h := sha512.New()
if _, err := io.Copy(h, resp.Body); err != nil {
return "", err
}
return hex.EncodeToString(h.Sum(nil)), nil
}
const detailsTmpl = `SPELL={{.Name}}
VERSION={{.Version}}
SOURCE=${SPELL}-${VERSION}.tar.gz
SOURCE_URL[0]={{.SourceURL}}
SOURCE_HASH=sha512:{{.Hash}}
SOURCE_DIRECTORY="${BUILD_DIRECTORY}/${SPELL}-${VERSION}"
WEB_SITE={{.Website}}
ENTERED={{.Date}}
LICENSE[0]={{.License}}
SHORT="{{.Description}}"
cat << EOF
{{.LongDesc}}
EOF
`
const dependsTmpl = `{{range .Dependencies}}depends {{.}} ""
{{end}}`
const buildTmpl = `#!/bin/bash
# Standard BUILD script — edit as needed.
cd "$SOURCE_DIRECTORY" &&
./configure --prefix=/usr "$@" &&
make &&
make install
`
const configureTmpl = `#!/bin/bash
# CONFIGURE — interactive queries go here. The Go ICE engine reads these.
# config_query WGET_SSL "Enable SSL support?" y
`