diff --git a/go.mod b/go.mod index 2302c73..2dd5067 100644 --- a/go.mod +++ b/go.mod @@ -7,4 +7,4 @@ require ( git.urbach.dev/go/color v0.0.0-20250606151219-222306e0b534 ) -require golang.org/x/sys v0.33.0 // indirect \ No newline at end of file +require golang.org/x/sys v0.33.0 \ No newline at end of file diff --git a/src/cli/help.txt b/src/cli/help.txt index f05f366..0b74ab1 100644 --- a/src/cli/help.txt +++ b/src/cli/help.txt @@ -9,5 +9,5 @@ Commands: --os [os] cross-compile for OS: [linux|mac|windows] --verbose, -v show everything - run [directory | file] build and run the executable + run [directory | file] build and run the executable in memory help show this help \ No newline at end of file diff --git a/src/cli/run.go b/src/cli/run.go index e97ddff..0ca58b2 100644 --- a/src/cli/run.go +++ b/src/cli/run.go @@ -3,10 +3,10 @@ package cli import ( "fmt" "os" - "os/exec" "git.urbach.dev/cli/q/src/compiler" "git.urbach.dev/cli/q/src/linker" + "git.urbach.dev/cli/q/src/memfile" ) // run builds and runs the executable. @@ -17,23 +17,20 @@ func run(args []string) int { return exit(err) } - result, err := compiler.Compile(b) + env, err := compiler.Compile(b) if err != nil { return exit(err) } - err = linker.WriteFile(b.Executable(), b, result) + file, err := memfile.New(b.Executable()) if err != nil { return exit(err) } - cmd := exec.Command(b.Executable()) - cmd.Stdout = os.Stdout - cmd.Stdin = os.Stdin - cmd.Stderr = os.Stderr - err = cmd.Run() + linker.Write(file, b, env) + err = memfile.Exec(file) if err != nil { fmt.Fprintln(os.Stderr, err) diff --git a/src/linker/Write.go b/src/linker/Write.go new file mode 100644 index 0000000..af55f8f --- /dev/null +++ b/src/linker/Write.go @@ -0,0 +1,41 @@ +package linker + +import ( + "io" + + "git.urbach.dev/cli/q/src/asm" + "git.urbach.dev/cli/q/src/build" + "git.urbach.dev/cli/q/src/core" + "git.urbach.dev/cli/q/src/data" + "git.urbach.dev/cli/q/src/elf" + "git.urbach.dev/cli/q/src/macho" + "git.urbach.dev/cli/q/src/pe" +) + +// Write writes an executable to the given writer. +func Write(writer io.WriteSeeker, b *build.Build, env *core.Environment) { + program := asm.Assembler{ + Instructions: make([]asm.Instruction, 0, 32), + Data: make(data.Data, 32), + } + + init := env.Functions["run.init"] + traversed := make(map[*core.Function]bool, len(env.Functions)) + + // This will place the init function immediately after the entry point + // and also add everything the init function calls recursively. + init.EachDependency(traversed, func(f *core.Function) { + program.Merge(&f.Assembler) + }) + + code, data, libs := program.Compile(b) + + switch b.OS { + case build.Linux: + elf.Write(writer, b, code, data) + case build.Mac: + macho.Write(writer, b, code, data) + case build.Windows: + pe.Write(writer, b, code, data, libs) + } +} \ No newline at end of file diff --git a/src/linker/WriteFile.go b/src/linker/WriteFile.go index 5edc0e1..93bc8ee 100644 --- a/src/linker/WriteFile.go +++ b/src/linker/WriteFile.go @@ -3,13 +3,8 @@ package linker import ( "os" - "git.urbach.dev/cli/q/src/asm" "git.urbach.dev/cli/q/src/build" "git.urbach.dev/cli/q/src/core" - "git.urbach.dev/cli/q/src/data" - "git.urbach.dev/cli/q/src/elf" - "git.urbach.dev/cli/q/src/macho" - "git.urbach.dev/cli/q/src/pe" ) // WriteFile writes an executable file to disk. @@ -20,36 +15,12 @@ func WriteFile(executable string, b *build.Build, env *core.Environment) error { return err } - init := env.Functions["run.init"] - traversed := make(map[*core.Function]bool, len(env.Functions)) - - final := asm.Assembler{ - Instructions: make([]asm.Instruction, 0, 32), - Data: make(data.Data, 32), - } - - // This will place the main function immediately after the entry point - // and also add everything the main function calls recursively. - init.EachDependency(traversed, func(f *core.Function) { - final.Merge(&f.Assembler) - }) - - code, data, libs := final.Compile(b) - - switch b.OS { - case build.Linux: - elf.Write(file, b, code, data) - case build.Mac: - macho.Write(file, b, code, data) - case build.Windows: - pe.Write(file, b, code, data, libs) - } - - err = file.Close() + Write(file, b, env) + err = file.Chmod(0o755) if err != nil { return err } - return os.Chmod(executable, 0o755) + return file.Close() } \ No newline at end of file diff --git a/src/memfile/Exec_linux.go b/src/memfile/Exec_linux.go new file mode 100644 index 0000000..91daf67 --- /dev/null +++ b/src/memfile/Exec_linux.go @@ -0,0 +1,47 @@ +//go:build linux + +package memfile + +import ( + "os" + "strconv" + "syscall" + "unsafe" + + "golang.org/x/sys/unix" +) + +// Exec executes an in-memory file. +func Exec(file *os.File) error { + empty, err := syscall.BytePtrFromString("") + + if err != nil { + return err + } + + argv := []string{"/proc/self/fd/" + strconv.Itoa(int(file.Fd()))} + argvp, err := syscall.SlicePtrFromStrings(argv) + + if err != nil { + return err + } + + envv := os.Environ() + envvp, err := syscall.SlicePtrFromStrings(envv) + + if err != nil { + return err + } + + _, _, errno := syscall.Syscall6( + unix.SYS_EXECVEAT, + uintptr(file.Fd()), + uintptr(unsafe.Pointer(empty)), + uintptr(unsafe.Pointer(&argvp[0])), + uintptr(unsafe.Pointer(&envvp[0])), + uintptr(unix.AT_EMPTY_PATH), + 0, + ) + + return errno +} \ No newline at end of file diff --git a/src/memfile/Exec_other.go b/src/memfile/Exec_other.go new file mode 100644 index 0000000..bccdb59 --- /dev/null +++ b/src/memfile/Exec_other.go @@ -0,0 +1,29 @@ +//go:build !linux + +package memfile + +import ( + "os" + "os/exec" +) + +// Exec executes an in-memory file. +func Exec(file *os.File) error { + err := file.Chmod(0o700) + + if err != nil { + return err + } + + err = file.Close() + + if err != nil { + return err + } + + cmd := exec.Command(file.Name()) + cmd.Stdout = os.Stdout + cmd.Stdin = os.Stdin + cmd.Stderr = os.Stderr + return cmd.Run() +} \ No newline at end of file diff --git a/src/memfile/New_linux.go b/src/memfile/New_linux.go new file mode 100644 index 0000000..9e6fc59 --- /dev/null +++ b/src/memfile/New_linux.go @@ -0,0 +1,21 @@ +//go:build linux + +package memfile + +import ( + "os" + + "golang.org/x/sys/unix" +) + +// New creates a new anonymous in-memory file. +func New(name string) (*os.File, error) { + fd, err := unix.MemfdCreate(name, unix.MFD_CLOEXEC) + + if err != nil { + return nil, err + } + + memFile := os.NewFile(uintptr(fd), name) + return memFile, nil +} \ No newline at end of file diff --git a/src/memfile/New_other.go b/src/memfile/New_other.go new file mode 100644 index 0000000..26f9744 --- /dev/null +++ b/src/memfile/New_other.go @@ -0,0 +1,12 @@ +//go:build !linux + +package memfile + +import ( + "os" +) + +// New creates a new anonymous in-memory file. +func New(name string) (*os.File, error) { + return os.CreateTemp("", "") +} \ No newline at end of file diff --git a/src/memfile/memfile_test.go b/src/memfile/memfile_test.go new file mode 100644 index 0000000..9880b1f --- /dev/null +++ b/src/memfile/memfile_test.go @@ -0,0 +1,15 @@ +package memfile_test + +import ( + "testing" + + "git.urbach.dev/cli/q/src/memfile" + "git.urbach.dev/go/assert" +) + +func TestNew(t *testing.T) { + file, err := memfile.New("") + assert.Nil(t, err) + assert.NotNil(t, file) + // memfile.Exec can't be tested because it would replace the test executable +} \ No newline at end of file