// 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 `