Browse Source

feat: Packing of ECAL code into executable

Matthias Ladkau 3 years ago
parent
commit
ca80016dd6
5 changed files with 360 additions and 9 deletions
  1. 5 3
      cli/ecal.go
  2. 10 5
      cli/tool/interpret.go
  3. 344 0
      cli/tool/pack.go
  4. 1 1
      examples/fib/fib.ecal
  5. 0 0
      examples/fib/lib/lib.ecal

+ 5 - 3
cli/ecal.go

@@ -21,13 +21,14 @@ import (
 
 /*
 TODO:
-- create executable binary (pack into single binary)
 - pretty printer
 - reload on start for debugger adapter
 */
 
 func main() {
 
+	tool.RunPackedBinary() // See if we try to run a standalone binary
+
 	// Initialize the default command line parser
 
 	flag.CommandLine.Init(os.Args[0], flag.ContinueOnError)
@@ -69,6 +70,9 @@ func main() {
 			} else if arg == "debug" {
 				debugInterpreter := tool.NewCLIDebugInterpreter(interpreter)
 				err = debugInterpreter.Interpret()
+			} else if arg == "pack" {
+				packer := tool.NewCLIPacker()
+				err = packer.Pack()
 			} else {
 				flag.Usage()
 			}
@@ -82,7 +86,5 @@ func main() {
 			fmt.Println(fmt.Sprintf("Error: %v", err))
 		}
 
-	} else {
-		flag.Usage()
 	}
 }

+ 10 - 5
cli/tool/interpret.go

@@ -54,6 +54,8 @@ type CLIInterpreter struct {
 	CustomWelcomeMessage string
 	CustomHelpString     string
 
+	EntryFile string // Entry file for the program
+
 	// Parameter these can either be set programmatically or via CLI args
 
 	Dir      *string // Root dir for interpreter
@@ -65,7 +67,7 @@ type CLIInterpreter struct {
 NewCLIInterpreter creates a new commandline interpreter for ECAL.
 */
 func NewCLIInterpreter() *CLIInterpreter {
-	return &CLIInterpreter{scope.NewScope(scope.GlobalScope), nil, nil, "", "", nil, nil, nil}
+	return &CLIInterpreter{scope.NewScope(scope.GlobalScope), nil, nil, "", "", "", nil, nil, nil}
 }
 
 /*
@@ -96,6 +98,10 @@ func (i *CLIInterpreter) ParseArgs() bool {
 	if len(os.Args) >= 2 {
 		flag.CommandLine.Parse(os.Args[2:])
 
+		if cargs := flag.Args(); len(cargs) > 0 {
+			i.EntryFile = flag.Arg(0)
+		}
+
 		if *showHelp {
 			flag.Usage()
 		}
@@ -164,14 +170,13 @@ func (i *CLIInterpreter) LoadInitialFile(tid uint64) error {
 
 	i.GlobalVS.Clear()
 
-	if cargs := flag.Args(); len(cargs) > 0 {
+	if i.EntryFile != "" {
 		var ast *parser.ASTNode
 		var initFile []byte
 
-		initFileName := flag.Arg(0)
-		initFile, err = ioutil.ReadFile(initFileName)
+		initFile, err = ioutil.ReadFile(i.EntryFile)
 
-		if ast, err = parser.ParseWithRuntime(initFileName, string(initFile), i.RuntimeProvider); err == nil {
+		if ast, err = parser.ParseWithRuntime(i.EntryFile, string(initFile), i.RuntimeProvider); err == nil {
 			if err = ast.Runtime.Validate(); err == nil {
 				_, err = ast.Runtime.Eval(i.GlobalVS, make(map[string]interface{}), tid)
 			}

+ 344 - 0
cli/tool/pack.go

@@ -0,0 +1,344 @@
+/*
+ * ECAL
+ *
+ * Copyright 2020 Matthias Ladkau. All rights reserved.
+ *
+ * This Source Code Form is subject to the terms of the MIT
+ * License, If a copy of the MIT License was not distributed with this
+ * file, You can obtain one at https://opensource.org/licenses/MIT.
+ */
+
+package tool
+
+import (
+	"archive/zip"
+	"flag"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+	"unicode"
+
+	"devt.de/krotik/common/errorutil"
+	"devt.de/krotik/common/fileutil"
+	"devt.de/krotik/ecal/interpreter"
+	"devt.de/krotik/ecal/parser"
+	"devt.de/krotik/ecal/scope"
+	"devt.de/krotik/ecal/util"
+)
+
+/*
+CLIPacker is a commandline packing tool for ECAL. This tool can build a self
+contained executable.
+*/
+type CLIPacker struct {
+	EntryFile string // Entry file for the program
+
+	// Parameter these can either be set programmatically or via CLI args
+
+	Dir          *string // Root dir for interpreter (all files will be collected)
+	SourceBinary *string // Binary which is used by the packer
+	TargetBinary *string // Binary which will be build by the packer
+}
+
+/*
+NewCLIPacker creates a new commandline packer.
+*/
+func NewCLIPacker() *CLIPacker {
+	return &CLIPacker{"", nil, nil, nil}
+}
+
+/*
+ParseArgs parses the command line arguments. Returns true if the program should exit.
+*/
+func (p *CLIPacker) ParseArgs() bool {
+
+	if p.Dir != nil && p.TargetBinary != nil && p.EntryFile != "" {
+		return false
+	}
+
+	binname, err := filepath.Abs(os.Args[0])
+	errorutil.AssertOk(err)
+
+	wd, _ := os.Getwd()
+
+	p.Dir = flag.String("dir", wd, "Root directory for ECAL interpreter")
+	p.SourceBinary = flag.String("source", binname, "Filename for source binary")
+	p.TargetBinary = flag.String("target", "out.bin", "Filename for target binary")
+	showHelp := flag.Bool("help", false, "Show this help message")
+
+	flag.Usage = func() {
+		fmt.Println()
+		fmt.Println(fmt.Sprintf("Usage of %s run [options] [entry file]", os.Args[0]))
+		fmt.Println()
+		flag.PrintDefaults()
+		fmt.Println()
+		fmt.Println("This tool will collect all files in the root directory and " +
+			"build a standalone executable from the given source binary and the collected files.")
+		fmt.Println()
+	}
+
+	if len(os.Args) >= 2 {
+		flag.CommandLine.Parse(os.Args[2:])
+
+		if cargs := flag.Args(); len(cargs) > 0 {
+			p.EntryFile = flag.Arg(0)
+		}
+
+		if *showHelp {
+			flag.Usage()
+		}
+	}
+
+	return *showHelp
+}
+
+/*
+Pack builds a standalone executable from a given source binary and collected files.
+*/
+func (p *CLIPacker) Pack() error {
+	if p.ParseArgs() {
+		return nil
+	}
+
+	fmt.Println(fmt.Sprintf("Packing %v -> %v from %v with entry: %v", *p.Dir,
+		*p.TargetBinary, *p.SourceBinary, p.EntryFile))
+
+	source, err := os.Open(*p.SourceBinary)
+	if err == nil {
+		var dest *os.File
+		defer source.Close()
+
+		if dest, err = os.Create(*p.TargetBinary); err == nil {
+			var bytes int64
+
+			defer dest.Close()
+
+			// First copy the binary
+
+			if bytes, err = io.Copy(dest, source); err == nil {
+				fmt.Println(fmt.Sprintf("Copied %v bytes for interpreter.", bytes))
+				var bytes int
+
+				end := "####"
+				marker := fmt.Sprintf("\n%v%v%v\n", end, "ECALSRC", end)
+
+				if bytes, err = dest.WriteString(marker); err == nil {
+					var data []byte
+					fmt.Println(fmt.Sprintf("Writing marker %v bytes for source archive.", bytes))
+
+					// Create a new zip archive.
+
+					w := zip.NewWriter(dest)
+
+					if data, err = ioutil.ReadFile(p.EntryFile); err == nil {
+						var f io.Writer
+						if f, err = w.Create(".ecalsrc-entry"); err == nil {
+							if bytes, err = f.Write(data); err == nil {
+								fmt.Println(fmt.Sprintf("Writing %v bytes for intro", bytes))
+
+								// Add files to the archive
+
+								if err = p.packFiles(w, *p.Dir, ""); err == nil {
+									err = w.Close()
+
+									os.Chmod(*p.TargetBinary, 0775) // Try a chmod but don't care about any errors
+								}
+							}
+						}
+					}
+				}
+			}
+		}
+	}
+
+	return err
+}
+
+/*
+packFiles walk through a given file structure and copies all files into a given zip writer.
+*/
+func (p *CLIPacker) packFiles(w *zip.Writer, filePath string, zipPath string) error {
+	var bytes int
+	files, err := ioutil.ReadDir(filePath)
+
+	if err == nil {
+		for _, file := range files {
+			if !file.IsDir() {
+				var data []byte
+				if data, err = ioutil.ReadFile(filepath.Join(filePath, file.Name())); err == nil {
+					var f io.Writer
+					if f, err = w.Create(filepath.Join(zipPath, file.Name())); err == nil {
+						if bytes, err = f.Write(data); err == nil {
+							fmt.Println(fmt.Sprintf("Writing %v bytes for %v",
+								bytes, filepath.Join(filePath, file.Name())))
+						}
+					}
+				}
+			} else if file.IsDir() {
+				p.packFiles(w, filepath.Join(filePath, file.Name()),
+					filepath.Join(zipPath, file.Name()))
+			}
+		}
+	}
+
+	return err
+}
+
+/*
+RunPackedBinary runs ECAL code is it has been attached to the currently running binary.
+Exits if attached ECAL code has been executed.
+*/
+func RunPackedBinary() {
+	var retCode = 0
+	var result bool
+
+	exename, err := filepath.Abs(os.Args[0])
+	errorutil.AssertOk(err)
+
+	end := "####"
+	marker := fmt.Sprintf("\n%v%v%v\n", end, "ECALSRC", end)
+
+	if ok, _ := fileutil.PathExists(exename); !ok {
+
+		// Try an optional .exe suffix which might work on Windows
+
+		exename += ".exe"
+	}
+
+	stat, err := os.Stat(exename)
+	if err == nil {
+		var f *os.File
+
+		if f, err = os.Open(exename); err == nil {
+			var pos int64
+
+			defer f.Close()
+
+			found := false
+			buf := make([]byte, 4096)
+			buf2 := make([]byte, len(marker)+11)
+
+			// Look for the marker which marks the beginning of the attached zip file
+
+			for i, err := f.Read(buf); err == nil; i, err = f.Read(buf) {
+
+				// Check if the marker could be in the read string
+
+				if strings.Contains(string(buf), "#") {
+
+					// Marker was found - read a bit more to ensure we got the full marker
+
+					if i2, err := f.Read(buf2); err == nil || err == io.EOF {
+						candidateString := string(append(buf, buf2...))
+
+						// Now determine the position if the zip file
+
+						markerIndex := strings.Index(candidateString, marker)
+
+						if found = markerIndex >= 0; found {
+							start := int64(markerIndex + len(marker))
+							for unicode.IsSpace(rune(candidateString[start])) || unicode.IsControl(rune(candidateString[start])) {
+								start++ // Skip final control characters \n or \r\n
+							}
+							pos += start
+							break
+						}
+
+						pos += int64(i2)
+					}
+				}
+
+				pos += int64(i)
+			}
+
+			if err == nil && found {
+
+				// Extract the zip
+
+				if _, err = f.Seek(pos, 0); err == nil {
+					var ret interface{}
+
+					zipLen := stat.Size() - pos
+
+					ret, err = runInterpreter(io.NewSectionReader(f, pos, zipLen), zipLen)
+
+					if retNum, ok := ret.(float64); ok {
+						retCode = int(retNum)
+					}
+
+					result = err == nil
+				}
+			}
+		}
+	}
+
+	errorutil.AssertOk(err)
+
+	if result {
+		os.Exit(retCode)
+	}
+}
+
+func runInterpreter(reader io.ReaderAt, size int64) (interface{}, error) {
+	var res interface{}
+	var rc io.ReadCloser
+
+	il := &util.MemoryImportLocator{Files: make(map[string]string)}
+
+	r, err := zip.NewReader(reader, size)
+
+	if err == nil {
+
+		for _, f := range r.File {
+
+			if rc, err = f.Open(); err == nil {
+				var data []byte
+
+				defer rc.Close()
+
+				if data, err = ioutil.ReadAll(rc); err == nil {
+					il.Files[f.Name] = string(data)
+				}
+			}
+
+			if err != nil {
+				break
+			}
+		}
+	}
+
+	if err == nil {
+		var ast *parser.ASTNode
+
+		erp := interpreter.NewECALRuntimeProvider(os.Args[0], il, util.NewStdOutLogger())
+
+		if ast, err = parser.ParseWithRuntime(os.Args[0], il.Files[".ecalsrc-entry"], erp); err == nil {
+			if err = ast.Runtime.Validate(); err == nil {
+				var osArgs []interface{}
+
+				vs := scope.NewScope(scope.GlobalScope)
+				for _, arg := range os.Args {
+					osArgs = append(osArgs, arg)
+				}
+				vs.SetValue("osArgs", osArgs)
+
+				res, err = ast.Runtime.Eval(vs, make(map[string]interface{}), erp.NewThreadID())
+
+				if err != nil {
+					fmt.Fprintln(os.Stderr, err.Error())
+
+					if terr, ok := err.(util.TraceableRuntimeError); ok {
+						fmt.Fprintln(os.Stderr, fmt.Sprint("  ", strings.Join(terr.GetTraceString(), fmt.Sprint(fmt.Sprintln(), "  "))))
+					}
+
+					err = nil
+				}
+			}
+		}
+	}
+
+	return res, err
+}

+ 1 - 1
examples/fib/fib.ecal

@@ -1,4 +1,4 @@
-import "lib.ecal" as lib
+import "lib/lib.ecal" as lib
 
 /* Print out fibonacci sequence */
 

examples/fib/lib.ecal → examples/fib/lib/lib.ecal