This commit is contained in:
sam 2024-09-19 02:12:57 +02:00
commit 9298fc4e4c
Signed by: sam
GPG key ID: B4EF20DDE721CAA1
12 changed files with 599 additions and 0 deletions

72
internal/build/builder.go Normal file
View file

@ -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
}

51
internal/build/fetcher.go Normal file
View file

@ -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
}

68
internal/build/tarball.go Normal file
View file

@ -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
}

View file

@ -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: "<name>",
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
}

View file

@ -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
}

111
internal/state/builtins.go Normal file
View file

@ -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
}

121
internal/state/state.go Normal file
View file

@ -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
}