From 9298fc4e4c95559d7b74355d344b966864beb09b Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 19 Sep 2024 02:12:57 +0200 Subject: [PATCH] init --- LICENSE | 28 +++++++ cmd/vlp/main.go | 26 +++++++ go.mod | 14 ++++ go.sum | 10 +++ internal/build/builder.go | 72 +++++++++++++++++ internal/build/fetcher.go | 51 ++++++++++++ internal/build/tarball.go | 68 ++++++++++++++++ internal/commands/build/command.go | 45 +++++++++++ internal/packages/packages.go | 38 +++++++++ internal/state/builtins.go | 111 ++++++++++++++++++++++++++ internal/state/state.go | 121 +++++++++++++++++++++++++++++ packages/vlp.lua | 15 ++++ 12 files changed, 599 insertions(+) create mode 100644 LICENSE create mode 100644 cmd/vlp/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/build/builder.go create mode 100644 internal/build/fetcher.go create mode 100644 internal/build/tarball.go create mode 100644 internal/commands/build/command.go create mode 100644 internal/packages/packages.go create mode 100644 internal/state/builtins.go create mode 100644 internal/state/state.go create mode 100644 packages/vlp.lua diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fdce7eb --- /dev/null +++ b/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2024, sam + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/cmd/vlp/main.go b/cmd/vlp/main.go new file mode 100644 index 0000000..feae780 --- /dev/null +++ b/cmd/vlp/main.go @@ -0,0 +1,26 @@ +package main + +import ( + "fmt" + "os" + + "code.vulpine.solutions/sam/vlp/internal/commands/build" + "github.com/urfave/cli/v2" +) + +var App = &cli.App{ + Name: "vlp", + Usage: "Package manager for foxes", + UseShortOptionHandling: true, + Commands: []*cli.Command{ + build.Command, + }, +} + +func main() { + err := App.Run(os.Args) + if err != nil { + fmt.Println("Error running vlp:", err) + os.Exit(1) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..36da5b2 --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module code.vulpine.solutions/sam/vlp + +go 1.23.1 + +require ( + github.com/urfave/cli/v2 v2.27.4 + github.com/yuin/gopher-lua v1.1.1 +) + +require ( + github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c3ff29c --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8= +github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= +github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= diff --git a/internal/build/builder.go b/internal/build/builder.go new file mode 100644 index 0000000..d4c409c --- /dev/null +++ b/internal/build/builder.go @@ -0,0 +1,72 @@ +package build + +import ( + "fmt" + "os" + "os/exec" + + "code.vulpine.solutions/sam/vlp/internal/packages" +) + +type Builder struct { + Name string + Version string + Dependencies []packages.Dependency + + SourceFetcher Fetcher + Commands []BuildCommand + Binaries []string + + TempDir string +} + +type BuildCommand struct { + Command string + Args []string +} + +func New(name, version string) (*Builder, error) { + b := &Builder{ + Name: name, + Version: version, + } + + tmp, err := tmpDir(name, version) + if err != nil { + return nil, err + } + + b.TempDir = tmp + return b, nil +} + +func (b *Builder) Build() error { + for i, cmd := range b.Commands { + exitCode, out := execCommand(cmd.Command, cmd.Args...) + if exitCode != 0 { + return fmt.Errorf("command %d returned exit code %d: %v", i, exitCode, string(out)) + } + } + return nil +} + +func tmpDir(name, version string) (string, error) { + tmp, err := os.MkdirTemp(os.TempDir(), "vlp-build-"+name+"-"+version+"_") + if err != nil { + return "", fmt.Errorf("creating temp dir: %w", err) + } + return tmp, nil +} + +func execCommand(bin string, args ...string) (int, []byte) { + err := exec.Command(bin, args...).Run() + if err != nil { + eErr, ok := err.(*exec.ExitError) + if ok { + return eErr.ExitCode(), eErr.Stderr + } + + return 1, nil + } + return 0, nil +} diff --git a/internal/build/fetcher.go b/internal/build/fetcher.go new file mode 100644 index 0000000..6d5bd40 --- /dev/null +++ b/internal/build/fetcher.go @@ -0,0 +1,51 @@ +package build + +import ( + "fmt" + "os" + "path/filepath" +) + +type Fetcher interface { + Fetch(tempDir string) error +} + +type BlankFetcher struct{} + +func (BlankFetcher) Fetch(tempDir string) error { + return fmt.Errorf("no sources were configured") +} + +var _ Fetcher = (*GitFetcher)(nil) + +type GitFetcher struct { + Repository, Ref string +} + +func (g GitFetcher) Fetch(tempDir string) error { + + repoDir := filepath.Join(tempDir, "repo") + exitCode, out := execCommand("git", "clone", "--depth=1", g.Repository, repoDir) + if exitCode != 0 { + return fmt.Errorf("git clone returned %d, output: %v", exitCode, string(out)) + } + + err := os.Chdir(repoDir) + if err != nil { + return fmt.Errorf("changing to repository directory: %w", err) + } + + if g.Ref != "" { + exitCode, out = execCommand("git", "fetch", "--depth=1", "origin", g.Ref) + if exitCode != 0 { + return fmt.Errorf("fetch clone returned %d, output: %v", exitCode, string(out)) + } + + exitCode, out = execCommand("git", "checkout", g.Ref) + if exitCode != 0 { + return fmt.Errorf("git checkout returned %d, output: %v", exitCode, string(out)) + } + } + + return nil +} diff --git a/internal/build/tarball.go b/internal/build/tarball.go new file mode 100644 index 0000000..77b700f --- /dev/null +++ b/internal/build/tarball.go @@ -0,0 +1,68 @@ +package build + +import ( + "archive/tar" + "bytes" + "fmt" + "io" + "os" + "path/filepath" + "runtime" +) + +func (b *Builder) GenerateTarball() error { + buf := new(bytes.Buffer) + + tarW := tar.NewWriter(buf) + + err := tarW.WriteHeader(&tar.Header{ + Name: "bin/", + Mode: int64(os.ModeDir) | 0o755, + }) + if err != nil { + return fmt.Errorf("writing bin directory: %w", err) + } + + for _, bin := range b.Binaries { + base := filepath.Base(bin) + + binB, err := os.ReadFile(bin) + if err != nil { + return fmt.Errorf("reading binary %v: %w", bin, err) + } + + err = tarW.WriteHeader(&tar.Header{ + Name: filepath.Join("bin", base), + Size: int64(len(binB)), + Mode: 0o755, + }) + if err != nil { + return fmt.Errorf("writing header for binary %v: %w", bin, err) + } + + _, err = tarW.Write(binB) + if err != nil { + return fmt.Errorf("writing binary %v: %w", bin, err) + } + } + + err = tarW.Close() + if err != nil { + return fmt.Errorf("writing tar footer: %w", err) + } + + f, err := os.Create(filepath.Join(b.TempDir, fmt.Sprintf("%v-%v-%v.tar", b.Name, b.Version, runtime.GOARCH))) + if err != nil { + return fmt.Errorf("creating tarball: %w", err) + } + defer f.Close() + + _, err = io.Copy(f, buf) + if err != nil { + return fmt.Errorf("writing to tarball: %w", err) + } + + fmt.Println("wrote tarball at", f.Name()) + + return nil +} diff --git a/internal/commands/build/command.go b/internal/commands/build/command.go new file mode 100644 index 0000000..6eef222 --- /dev/null +++ b/internal/commands/build/command.go @@ -0,0 +1,45 @@ +package build + +import ( + "fmt" + + "code.vulpine.solutions/sam/vlp/internal/packages" + "code.vulpine.solutions/sam/vlp/internal/state" + "github.com/urfave/cli/v2" +) + +var Command = &cli.Command{ + Name: "build", + Aliases: []string{"b"}, + Usage: "Build a package from source", + Args: true, + ArgsUsage: "", + Action: action, +} + +func action(ctx *cli.Context) error { + if ctx.NArg() < 1 { + return cli.Exit("Not enough arguments", 1) + } + + name := ctx.Args().Get(0) + + script, err := packages.LookupPackageFile(name) + if err != nil { + return err + } + + s := state.New(name) + err = s.Eval(script) + if err != nil { + return err + } + + err = s.Build() + if err != nil { + return err + } + + fmt.Println(s.Builder.Name, s.Builder.Dependencies) + return nil +} diff --git a/internal/packages/packages.go b/internal/packages/packages.go new file mode 100644 index 0000000..6a1feba --- /dev/null +++ b/internal/packages/packages.go @@ -0,0 +1,38 @@ +package packages + +import ( + "bytes" + "fmt" + "io" + "os" + "path/filepath" +) + +type Package struct { + Name string + Dependencies []Dependency +} + +type Dependency struct { + Name string + Version string +} + +func LookupPackageFile(name string) (io.Reader, error) { + path := filepath.Join("./packages", name+".lua") + + f, err := os.Open(path) + if err != nil { + return nil, fmt.Errorf("opening package file: %w", err) + } + defer f.Close() + + buf := new(bytes.Buffer) + + _, err = io.Copy(buf, f) + if err != nil { + return nil, fmt.Errorf("reading package file: %w", err) + } + + return buf, nil +} diff --git a/internal/state/builtins.go b/internal/state/builtins.go new file mode 100644 index 0000000..b36c289 --- /dev/null +++ b/internal/state/builtins.go @@ -0,0 +1,111 @@ +package state + +import ( + "code.vulpine.solutions/sam/vlp/internal/build" + "code.vulpine.solutions/sam/vlp/internal/packages" + lua "github.com/yuin/gopher-lua" +) + +func (s *State) depends(ls *lua.LState) int { + t := ls.GetTop() + if t == 0 { + return 0 + } + + for i := 1; i <= t; i++ { + table, ok := ls.Get(i).(*lua.LTable) + if !ok { + ls.RaiseError("Argument %v was not a table", i) + return 0 + } + + var ( + name string + version = "*" + ) + + switch table.Len() { + default: + fallthrough + case 2: + version = table.RawGetInt(2).String() + fallthrough + case 1: + name = table.RawGetInt(1).String() + case 0: + ls.RaiseError("Table %v is empty", i) + return 0 + } + + s.Builder.Dependencies = append(s.Builder.Dependencies, packages.Dependency{Name: name, Version: version}) + } + + return 0 +} + +func (s *State) git(ls *lua.LState) int { + t := ls.GetTop() + if t == 0 { + ls.RaiseError("`git` needs a repository URL") + return 0 + } + + var repo, ref string + + lrepo, ok := ls.Get(1).(lua.LString) + if !ok { + ls.RaiseError("Argument 1 is not a string") + return 0 + } + repo = lrepo.String() + + lref, ok := ls.Get(2).(lua.LString) + if ok { + ref = lref.String() + } + + s.Builder.SourceFetcher = build.GitFetcher{Repository: repo, Ref: ref} + return 0 +} + +func (s *State) cmd(ls *lua.LState) int { + t := ls.GetTop() + if t == 0 { + ls.RaiseError("`cmd` needs a command to run") + return 0 + } + + lcmd, ok := ls.Get(1).(lua.LString) + if !ok { + ls.RaiseError("Argument 1 is not a string") + return 0 + } + + var args []string + for i := 2; i <= t; i++ { + larg := ls.Get(i) + if larg == lua.LNil { + break + } + args = append(args, larg.String()) + } + + s.Builder.Commands = append(s.Builder.Commands, build.BuildCommand{Command: lcmd.String(), Args: args}) + return 0 +} + +func (s *State) bin(ls *lua.LState) int { + if ls.GetTop() != 1 { + ls.RaiseError("`bin` needs a single binary to install") + return 0 + } + + lbin, ok := ls.Get(1).(lua.LString) + if !ok { + ls.RaiseError("Argument 1 is not a string") + return 0 + } + + s.Builder.Binaries = append(s.Builder.Binaries, lbin.String()) + return 0 +} diff --git a/internal/state/state.go b/internal/state/state.go new file mode 100644 index 0000000..ef9f0a1 --- /dev/null +++ b/internal/state/state.go @@ -0,0 +1,121 @@ +package state + +import ( + "fmt" + "io" + + "code.vulpine.solutions/sam/vlp/internal/build" + lua "github.com/yuin/gopher-lua" +) + +type State struct { + Lua *lua.LState + Builder *build.Builder +} + +func New(packageName string) *State { + s := &State{ + Lua: lua.NewState(lua.Options{ + IncludeGoStackTrace: true, + }), + Builder: &build.Builder{Name: packageName}, + } + + s.Lua.SetGlobal("depends", s.Lua.NewFunction(s.depends)) + s.Lua.SetGlobal("git", s.Lua.NewFunction(s.git)) + s.Lua.SetGlobal("cmd", s.Lua.NewFunction(s.cmd)) + s.Lua.SetGlobal("bin", s.Lua.NewFunction(s.bin)) + + return s +} + +// Eval evaluates a build script. It does not actually build the package yet. +func (s *State) Eval(r io.Reader) error { + script, err := io.ReadAll(r) + if err != nil { + return fmt.Errorf("reading script: %w", err) + } + + err = s.Lua.DoString(string(script)) + if err != nil { + return fmt.Errorf("executing script: %w", err) + } + + var name, version string + + lname := s.Lua.GetGlobal("name") + if nameStr, ok := lname.(lua.LString); ok { + name = nameStr.String() + } else { + name = s.Builder.Name + } + + lversion := s.Lua.GetGlobal("version") + if versionStr, ok := lversion.(lua.LString); ok { + version = versionStr.String() + } + + s.Builder, err = build.New(name, version) + if err != nil { + return fmt.Errorf("creating builder: %w", err) + } + + return nil +} + +func (s *State) Build() error { + fetchFn, ok := s.Lua.GetGlobal("fetch").(*lua.LFunction) + if !ok { + return fmt.Errorf("`fetch` was not defined") + } + + err := s.Lua.CallByParam(lua.P{ + Fn: fetchFn, + NRet: 0, + Protect: true, + }) + if err != nil { + return fmt.Errorf("running fetch function: %w", err) + } + + err = s.Builder.SourceFetcher.Fetch(s.Builder.TempDir) + if err != nil { + return fmt.Errorf("fetching source: %w", err) + } + + buildFn := s.Lua.GetGlobal("build") + if buildFn != lua.LNil { + err := s.Lua.CallByParam(lua.P{ + Fn: buildFn, + NRet: 0, + Protect: true, + }) + if err != nil { + return fmt.Errorf("running build function: %w", err) + } + } + + err = s.Builder.Build() + if err != nil { + return fmt.Errorf("building source: %w", err) + } + + installFn := s.Lua.GetGlobal("install") + if installFn != lua.LNil { + err := s.Lua.CallByParam(lua.P{ + Fn: installFn, + NRet: 0, + Protect: true, + }) + if err != nil { + return fmt.Errorf("running install function: %w", err) + } + } + + err = s.Builder.GenerateTarball() + if err != nil { + return fmt.Errorf("generating zip: %w", err) + } + + return nil +} diff --git a/packages/vlp.lua b/packages/vlp.lua new file mode 100644 index 0000000..5c2b58b --- /dev/null +++ b/packages/vlp.lua @@ -0,0 +1,15 @@ +version = "0.1.0" + +depends({"go", "1.23"}, {"hello", "*"}) + +function fetch() + git("https://code.vulpine.solutions/sam/vlp.git") +end + +function build() + cmd("go", "build", "./cmd/vlp") +end + +function install() + bin("vlp") +end