small pixel drawing of a pufferfish zoa

reorg
Jes Olson j3s@c3f.net
Tue, 27 Sep 2022 18:51:31 -0500
commit

99753e24181949f8557f04fefb623369e2810325

parent

42b0b6dca257aeb73c8d7872279d0d05df5cd4e9

6 files changed, 355 insertions(+), 323 deletions(-)

jump to
A env/env.go

@@ -0,0 +1,136 @@

+package env + +import ( + "bufio" + "fmt" + "os" + "strings" + "syscall" + + "mvdan.cc/sh/v3/expand" +) + +func GenerateEnv() (expand.Environ, error) { + // syscall.Uname _should_ be supported on all *nix systems and is backed + // by posix + var env expand.Environ + uname := syscall.Utsname{} + err := syscall.Uname(&uname) + if err != nil { + return env, err + } + + // shell := "/bin/sh"? + + // $PATH is annoyingly non-standard, so we hardcode the var + // to the binary paths described in the fhs standard. + // in most cases, this will "just work" for people, but special + // cases should be evaluated - this may need some adjustment in the future. + // i am resistant to making it over-rideable. + // + // users can always call their special binaries with their full paths + // if they are resistant to moving them for some reason. + path := envString("PATH", "/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin") + uname_os := envString("OS", charsToString(uname.Sysname[:])) + uname_release := envString("RELEASE", charsToString(uname.Release[:])) + uname_arch := envString("ARCH", charsToString(uname.Machine[:])) + if err != nil { + return env, err + } + + // standards are extremely annoying about hostnames. + // + // "Note that there is no standard that + // says that the hostname set by sethostname(2) + // is the same string as the nodename field of + // the struct returned by uname() (indeed, some + // systems allow a 256-byte hostname and an 8-byte + // nodename), but this is true on Linux. The same + // holds for setdomainname(2) and the domainname field." + // + // in practice, there's usually not a difference between HOSTNAME + // and NODENAME, so i've chosen to only expose HOSTNAME for the + // sake of simplicity. i'm using the Golang implementation, which + // does call out to uname, annoyingly. + // + // if this becomes an issue, i'll revisit it. i doubt it though. + // tldr: i'm ignoring that golang's os.Hostname() implementation + // isn't standards-compliant by the letter of the law. + // in actual practice, the hostname and nodename + // are always identical in every case i've observed. + // + // and i'm exposing only 1 because otherwise things get annoying. + // shrug. + h, err := os.Hostname() + if err != nil { + return env, err + } + uname_hostname := fmt.Sprintf("HOSTNAME=%s", h) + + // !OS_RELEASE_* VARS ARE NOT STANDARDS-BACKED! + // OS_* vars may or may not exist depending on the distro in question, so + // they're not reliable _at all_. they're scraped from /etc/os-release + // and are useful for identifying specific Linux distros, or their versions. + // + // if you rely on these variables, I highly suggest checking for their + // existence with test -z before utilizing them. there be no standards here. + os_release, err := getOSRelease() + if err != nil { + return env, err + } + osReleaseID := envString("OS_RELEASE_ID", os_release.ID) + osReleaseVersionID := envString("OS_RELEASE_VERSION_ID", os_release.VersionID) + + env = expand.ListEnviron(path, // normie shit + uname_os, uname_hostname, uname_release, uname_arch, // uname-derivated env vars + osReleaseID, osReleaseVersionID) // /etc/os-release + return env, nil +} + +func envString(key string, value string) string { + return fmt.Sprintf("%s=%s", key, value) +} + +func charsToString(arr []int8) string { + b := make([]byte, 0, len(arr)) + for _, v := range arr { + if v == 0x00 { + break + } + b = append(b, byte(v)) + } + return string(b) +} + +// I want to keep this list as small as possible, since +// this struct is unreliable. +// design principle: only 1 way to do common things +type OSRelease struct { + ID string // distro name - "arch" + VersionID string // for debian distros, this is set to "22.04" +} + +// getOsRelease parses /etc/os-release data +// into a struct +func getOSRelease() (OSRelease, error) { + var osr = OSRelease{} + f, err := os.Open("/etc/os-release") + if err != nil { + return osr, err + } + defer f.Close() + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + key, value, _ := strings.Cut(scanner.Text(), "=") + value = strings.Trim(value, `"`) + switch key { + case "ID": + osr.ID = value + case "VERSION_ID": + osr.VersionID = value + } + } + + return osr, nil +}
M go.modgo.mod

@@ -2,9 +2,10 @@ module j3s.sh/zoa

go 1.19 +require mvdan.cc/sh/v3 v3.5.1 + require ( golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect - mvdan.cc/sh/v3 v3.5.1 // indirect )
M go.sumgo.sum

@@ -1,8 +1,15 @@

+github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= +github.com/frankban/quicktest v1.14.0 h1:+cqqvzZV87b4adx/5ayVOaYZ2CrvM4ejQvUdBzPPUss= +github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= mvdan.cc/sh/v3 v3.5.1 h1:hmP3UOw4f+EYexsJjFxvU38+kn+V/s2CclXHanIBkmQ= mvdan.cc/sh/v3 v3.5.1/go.mod h1:1JcoyAKm1lZw/2bZje/iYKWicU/KMd0rsyJeKHnsK4E=
M main.gomain.go

@@ -1,24 +1,17 @@

package main import ( - "bufio" - "bytes" - "context" - "crypto/sha256" "fmt" - "io" "log" "os" "path/filepath" - "strings" - "syscall" + + "j3s.sh/zoa/env" + "j3s.sh/zoa/shell" - "mvdan.cc/sh/v3/expand" "mvdan.cc/sh/v3/interp" - "mvdan.cc/sh/v3/syntax" ) -var ctx = context.Background() var rootDir = "test/" func main() {

@@ -38,322 +31,12 @@ log.Fatal(err)

} // set standard env vars for runtime - r.Env, err = generateEnv() + r.Env, err = env.GenerateEnv() if err != nil { log.Fatal(err) } // for debuggin' fmt.Printf("%+v", r.Env) entrypoint := filepath.Join(rootDir, "main") - runCommands(entrypoint, r) -} - -func generateEnv() (expand.Environ, error) { - // syscall.Uname _should_ be supported on all *nix systems and is backed - // by posix - var env expand.Environ - uname := syscall.Utsname{} - err := syscall.Uname(&uname) - if err != nil { - return env, err - } - - // shell := "/bin/sh"? - - // $PATH is annoyingly non-standard, so we hardcode the var - // to the binary paths described in the fhs standard. - // in most cases, this will "just work" for people, but special - // cases should be evaluated - this may need some adjustment in the future. - // i am resistant to making it over-rideable. - // - // users can always call their special binaries with their full paths - // if they are resistant to moving them for some reason. - path := envString("PATH", "/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin") - uname_os := envString("OS", charsToString(uname.Sysname[:])) - uname_release := envString("RELEASE", charsToString(uname.Release[:])) - uname_arch := envString("ARCH", charsToString(uname.Machine[:])) - if err != nil { - return env, err - } - - // standards are extremely annoying about hostnames. - // - // "Note that there is no standard that - // says that the hostname set by sethostname(2) - // is the same string as the nodename field of - // the struct returned by uname() (indeed, some - // systems allow a 256-byte hostname and an 8-byte - // nodename), but this is true on Linux. The same - // holds for setdomainname(2) and the domainname field." - // - // in practice, there's usually not a difference between HOSTNAME - // and NODENAME, so i've chosen to only expose HOSTNAME for the - // sake of simplicity. i'm using the Golang implementation, which - // does call out to uname, annoyingly. - // - // if this becomes an issue, i'll revisit it. i doubt it though. - // tldr: i'm ignoring that golang's os.Hostname() implementation - // isn't standards-compliant by the letter of the law. - // in actual practice, the hostname and nodename - // are always identical in every case i've observed. - // - // and i'm exposing only 1 because otherwise things get annoying. - // shrug. - h, err := os.Hostname() - if err != nil { - return env, err - } - uname_hostname := fmt.Sprintf("HOSTNAME=%s", h) - - // !OS_RELEASE_* VARS ARE NOT STANDARDS-BACKED! - // OS_* vars may or may not exist depending on the distro in question, so - // they're not reliable _at all_. they're scraped from /etc/os-release - // and are useful for identifying specific Linux distros, or their versions. - // - // if you rely on these variables, I highly suggest checking for their - // existence with test -z before utilizing them. there be no standards here. - os_release, err := getOSRelease() - if err != nil { - return env, err - } - osReleaseID := envString("OS_RELEASE_ID", os_release.ID) - osReleaseVersionID := envString("OS_RELEASE_VERSION_ID", os_release.VersionID) - - env = expand.ListEnviron(path, // normie shit - uname_os, uname_hostname, uname_release, uname_arch, // uname-derivated env vars - osReleaseID, osReleaseVersionID) // /etc/os-release - return env, nil -} - -func envString(key string, value string) string { - return fmt.Sprintf("%s=%s", key, value) -} - -// I want to keep this list as small as possible, since -// this struct is unreliable. -// design principle: only 1 way to do common things -type OSRelease struct { - ID string // distro name - "arch" - VersionID string // for debian distros, this is set to "22.04" -} - -// getOsRelease parses /etc/os-release data -// into a struct -func getOSRelease() (OSRelease, error) { - var osr = OSRelease{} - f, err := os.Open("/etc/os-release") - if err != nil { - return osr, err - } - defer f.Close() - - scanner := bufio.NewScanner(f) - for scanner.Scan() { - key, value, _ := strings.Cut(scanner.Text(), "=") - value = strings.Trim(value, `"`) - switch key { - case "ID": - osr.ID = value - case "VERSION_ID": - osr.VersionID = value - } - } - - return osr, nil -} - -// this is used to detect when the script -// name changes from run to run, which allows -// us to prettily-print -var lastScriptPath string - -func runCommands(scriptPath string, r *interp.Runner) { - script, err := parseFile(scriptPath) - if err != nil { - fmt.Println("error in " + scriptPath) - fmt.Println(err) - os.Exit(1) - } - - // execute every statement individually, decorating - // each with ->, and doing some speshul logicks against - // certain strings - for _, stmt := range script.Stmts { - cmdName := commandName(stmt) - command, after, _ := strings.Cut(cmdName, " ") - - if command == "zoa-script" { - // recursion detected!! :3 - subScriptPath := filepath.Join(rootDir + "scripts/" + after) - runCommands(subScriptPath, r) - continue - } - - if command == "zoa-file" { - // after = "nginx /etc/nginx/conf.d/jesse.conf systemctl nginx reload" - zoaFileParts := strings.Split(after, " ") - if len(zoaFileParts) < 2 { - log.Fatal("zoa-file requires 2+ arguments") - } - src := zoaFileParts[0] - dst := zoaFileParts[1] - optionalCmd := "" - if len(zoaFileParts) > 2 { - optionalCmd = strings.Join(zoaFileParts[2:], " ") - } - fmt.Printf("$ zoa-file %s %s\n", src, dst) - dstChanged, err := zoaCopy(src, dst) - if err != nil { - log.Fatal(err) - } - // if there's an optional argument - if optionalCmd != "" && dstChanged { - re := strings.NewReader(optionalCmd) - f, err := syntax.NewParser().Parse(re, "") - if err != nil { - log.Fatal(err) - } - - for _, stmt := range f.Stmts { - runCommand(ctx, stmt, r) - } - } - return - } - - // if the script name changed between runs, - // print it - if scriptPath != lastScriptPath { - bluePrintln("-> " + scriptPath) - lastScriptPath = scriptPath - } - - fmt.Printf("$ %s\n", cmdName) - err = r.Run(ctx, stmt) - if err != nil { - // ignore err here bc it's just the status code - os.Exit(1) - } - } -} - -// zoaCopy copies a file from zoaRoot/scripts/src to dst. -// if the dst was changed, zoaCopy will return true, -// otherwise it will return false -// zoaCopy defaults to 0666 for permissions (before umask) -func zoaCopy(src string, dst string) (bool, error) { - src = filepath.Join(rootDir + "scripts/" + src) - srcChk, err := checksumFile(src) - if err != nil { - // source file should always exist, return error - return false, err - } - dstChk, err := checksumFile(dst) - if err != nil { - // dstfile may not exist for a million - // reasons, set checksum to blank - // to force a mismatch - dstChk = "" - } - - if srcChk == dstChk { - fmt.Println("file unchanged") - return false, nil - } - // TODO: pass the file through a templating engine - // - expand vars - // - loop support? - - err = Copy(src, dst) - if err != nil { - return false, err - } - return true, nil -} - -// Copy just copiez files, it's only used by zoaCopy rn -func Copy(src, dst string) error { - in, err := os.Open(src) - if err != nil { - return err - } - defer in.Close() - - out, err := os.Create(dst) - if err != nil { - return err - } - defer out.Close() - - _, err = io.Copy(out, in) - if err != nil { - return err - } - return out.Close() -} - -func checksumFile(file string) (string, error) { - f, err := os.Open(file) - if err != nil { - return "", err - } - defer f.Close() - - h := sha256.New() - if _, err := io.Copy(h, f); err != nil { - return "", err - } - - checksum := fmt.Sprintf("%x", h.Sum(nil)) - return checksum, nil -} - -func runCommand(c context.Context, s *syntax.Stmt, r *interp.Runner) { - name := commandName(s) - fmt.Printf("$ %s\n", name) - err := r.Run(c, s) - if err != nil { - os.Exit(1) - } -} - -func commandName(statement *syntax.Stmt) string { - b := new(bytes.Buffer) - syntax.NewPrinter().Print(b, statement) - return b.String() -} - -func parseFile(filename string) (*syntax.File, error) { - var result = &syntax.File{} - f, err := os.Open(filename) - if err != nil { - return result, err - } - defer f.Close() - result, err = syntax.NewParser().Parse(f, "") - return result, err -} - -func bluePrintln(s string) { - colored := fmt.Sprintf("\x1b[%dm%s\x1b[0m", 34, s) - fmt.Println(colored) -} - -// runStatements takes a file & runs individual -// commands from that file, prepending the decorator -// and returning the first error -// func runScript(file *syntax.File) error { -// fmt.Printf("%s%s\n", decorator, output) -// return nil -// } - -func charsToString(arr []int8) string { - b := make([]byte, 0, len(arr)) - for _, v := range arr { - if v == 0x00 { - break - } - b = append(b, byte(v)) - } - return string(b) + shell.RunCommands(entrypoint, r) }
A shell/shell.go

@@ -0,0 +1,155 @@

+package shell + +import ( + "bytes" + "context" + "fmt" + "log" + "os" + "path/filepath" + "strings" + + "j3s.sh/zoa/utils" + + "mvdan.cc/sh/v3/interp" + "mvdan.cc/sh/v3/syntax" +) + +// this is used to detect when the script +// name changes from run to run, which allows +// us to prettily-print +var lastScriptPath string +var ctx = context.Background() + +// TODO: make dis a git dingy +var rootDir = "test/" + +func RunCommands(scriptPath string, r *interp.Runner) { + script, err := parseFile(scriptPath) + if err != nil { + fmt.Println("error in " + scriptPath) + fmt.Println(err) + os.Exit(1) + } + + // execute every statement individually, decorating + // each with ->, and doing some speshul logicks against + // certain strings + for _, stmt := range script.Stmts { + cmdName := commandName(stmt) + command, after, _ := strings.Cut(cmdName, " ") + + if command == "zoa-script" { + // recursion detected!! :3 + subScriptPath := filepath.Join(rootDir + "scripts/" + after) + RunCommands(subScriptPath, r) + continue + } + + if command == "zoa-file" { + // after = "nginx /etc/nginx/conf.d/jesse.conf systemctl nginx reload" + zoaFileParts := strings.Split(after, " ") + if len(zoaFileParts) < 2 { + log.Fatal("zoa-file requires 2+ arguments") + } + src := zoaFileParts[0] + dst := zoaFileParts[1] + optionalCmd := "" + if len(zoaFileParts) > 2 { + optionalCmd = strings.Join(zoaFileParts[2:], " ") + } + fmt.Printf("$ zoa-file %s %s\n", src, dst) + dstChanged, err := zoaCopy(src, dst) + if err != nil { + log.Fatal(err) + } + // if there's an optional argument + if optionalCmd != "" && dstChanged { + re := strings.NewReader(optionalCmd) + f, err := syntax.NewParser().Parse(re, "") + if err != nil { + log.Fatal(err) + } + + for _, stmt := range f.Stmts { + runCommand(ctx, stmt, r) + } + } + return + } + + // if the script name changed between runs, + // print it + if scriptPath != lastScriptPath { + utils.BluePrintln("-> " + scriptPath) + lastScriptPath = scriptPath + } + + fmt.Printf("$ %s\n", cmdName) + err = r.Run(ctx, stmt) + if err != nil { + // ignore err here bc it's just the status code + os.Exit(1) + } + } +} + +func parseFile(filename string) (*syntax.File, error) { + var result = &syntax.File{} + f, err := os.Open(filename) + if err != nil { + return result, err + } + defer f.Close() + result, err = syntax.NewParser().Parse(f, "") + return result, err +} + +func runCommand(c context.Context, s *syntax.Stmt, r *interp.Runner) { + name := commandName(s) + fmt.Printf("$ %s\n", name) + err := r.Run(c, s) + if err != nil { + os.Exit(1) + } +} + +func commandName(statement *syntax.Stmt) string { + b := new(bytes.Buffer) + syntax.NewPrinter().Print(b, statement) + return b.String() +} + +// zoaCopy copies a file from zoaRoot/scripts/src to dst. +// if the dst was changed, zoaCopy will return true, +// otherwise it will return false +// zoaCopy defaults to 0666 for permissions (before umask) +func zoaCopy(src string, dst string) (bool, error) { + src = filepath.Join(rootDir + "scripts/" + src) + srcChk, err := utils.ChecksumFile(src) + if err != nil { + // source file should always exist, return error + return false, err + } + dstChk, err := utils.ChecksumFile(dst) + if err != nil { + // dstfile may not exist for a million + // reasons, set checksum to blank + // to force a mismatch + dstChk = "" + } + + if srcChk == dstChk { + fmt.Println("file unchanged") + return false, nil + } + // TODO: pass the file through a templating engine + // - expand vars + // - loop support? + + err = utils.Copy(src, dst) + if err != nil { + return false, err + } + return true, nil +}
A utils/utils.go

@@ -0,0 +1,50 @@

+package utils + +import ( + "crypto/sha256" + "fmt" + "io" + "os" +) + +// Copy just copiez files, it's only used by zoaCopy rn +func Copy(src, dst string) error { + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + + out, err := os.Create(dst) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, in) + if err != nil { + return err + } + return out.Close() +} + +func ChecksumFile(file string) (string, error) { + f, err := os.Open(file) + if err != nil { + return "", err + } + defer f.Close() + + h := sha256.New() + if _, err := io.Copy(h, f); err != nil { + return "", err + } + + checksum := fmt.Sprintf("%x", h.Sum(nil)) + return checksum, nil +} + +func BluePrintln(s string) { + colored := fmt.Sprintf("\x1b[%dm%s\x1b[0m", 34, s) + fmt.Println(colored) +}