Improved project structure
This commit is contained in:
110
src/build/Build.go
Normal file
110
src/build/Build.go
Normal file
@ -0,0 +1,110 @@
|
||||
package build
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"git.akyoto.dev/cli/q/src/directory"
|
||||
"git.akyoto.dev/cli/q/src/elf"
|
||||
"git.akyoto.dev/cli/q/src/errors"
|
||||
"git.akyoto.dev/cli/q/src/log"
|
||||
)
|
||||
|
||||
// Build describes a compiler build.
|
||||
type Build struct {
|
||||
Name string
|
||||
Directory string
|
||||
Code bytes.Buffer
|
||||
Data bytes.Buffer
|
||||
WriteExecutable bool
|
||||
}
|
||||
|
||||
// New creates a new build.
|
||||
func New(directory string) *Build {
|
||||
return &Build{
|
||||
Name: filepath.Base(directory),
|
||||
Directory: directory,
|
||||
WriteExecutable: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Run parses the input files and generates an executable file.
|
||||
func (build *Build) Run() error {
|
||||
err := build.Compile()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if build.WriteExecutable {
|
||||
return writeToDisk(build.Executable(), build.Code.Bytes(), build.Data.Bytes())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Compile compiles all the functions.
|
||||
func (build *Build) Compile() error {
|
||||
stat, err := os.Stat(build.Directory)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !stat.IsDir() {
|
||||
return &errors.InvalidDirectory{Path: build.Directory}
|
||||
}
|
||||
|
||||
directory.Walk(build.Directory, func(file string) {
|
||||
if !strings.HasSuffix(file, ".q") {
|
||||
return
|
||||
}
|
||||
|
||||
log.Info.Println(file)
|
||||
})
|
||||
|
||||
build.Code.Write([]byte{
|
||||
0xb8, 0x01, 0x00, 0x00, 0x00, // mov eax, 1
|
||||
0xbf, 0x01, 0x00, 0x00, 0x00, // mov edi, 1
|
||||
0xbe, 0xa2, 0x00, 0x40, 0x00, // mov esi, 0x4000a2
|
||||
0xba, 0x06, 0x00, 0x00, 0x00, // mov edx, 6
|
||||
0x0f, 0x05, // syscall
|
||||
|
||||
0xb8, 0x3c, 0x00, 0x00, 0x00, // mov eax, 60
|
||||
0xbf, 0x00, 0x00, 0x00, 0x00, // mov edi, 0
|
||||
0x0f, 0x05, // syscall
|
||||
})
|
||||
|
||||
build.Data.Write([]byte{'H', 'e', 'l', 'l', 'o', '\n'})
|
||||
return nil
|
||||
}
|
||||
|
||||
// Executable returns the path to the executable.
|
||||
func (build *Build) Executable() string {
|
||||
return filepath.Join(build.Directory, build.Name)
|
||||
}
|
||||
|
||||
// writeToDisk writes the executable file to disk.
|
||||
func writeToDisk(filePath string, code []byte, data []byte) error {
|
||||
file, err := os.Create(filePath)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buffer := bufio.NewWriter(file)
|
||||
executable := elf.New(code, data)
|
||||
executable.Write(buffer)
|
||||
buffer.Flush()
|
||||
|
||||
err = file.Close()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.Chmod(filePath, 0755)
|
||||
}
|
37
src/cli/Build.go
Normal file
37
src/cli/Build.go
Normal file
@ -0,0 +1,37 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"git.akyoto.dev/cli/q/src/build"
|
||||
"git.akyoto.dev/cli/q/src/log"
|
||||
)
|
||||
|
||||
// Build builds an executable.
|
||||
func Build(args []string) int {
|
||||
directory := "."
|
||||
|
||||
if len(args) > 0 {
|
||||
directory = args[0]
|
||||
}
|
||||
|
||||
b := build.New(directory)
|
||||
|
||||
for i := 1; i < len(args); i++ {
|
||||
switch args[i] {
|
||||
case "--dry":
|
||||
b.WriteExecutable = false
|
||||
|
||||
default:
|
||||
log.Error.Printf("Unknown parameter: %s\n", args[i])
|
||||
return 2
|
||||
}
|
||||
}
|
||||
|
||||
err := b.Run()
|
||||
|
||||
if err != nil {
|
||||
log.Error.Println(err)
|
||||
return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
14
src/cli/Help.go
Normal file
14
src/cli/Help.go
Normal file
@ -0,0 +1,14 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"git.akyoto.dev/cli/q/src/log"
|
||||
)
|
||||
|
||||
// Help shows the command line argument usage.
|
||||
func Help(args []string) int {
|
||||
log.Error.Println("Usage: q [command] [options]")
|
||||
log.Error.Println("")
|
||||
log.Error.Println(" build [directory]")
|
||||
log.Error.Println(" system")
|
||||
return 2
|
||||
}
|
21
src/cli/Main.go
Normal file
21
src/cli/Main.go
Normal file
@ -0,0 +1,21 @@
|
||||
package cli
|
||||
|
||||
// Main is the entry point for the CLI frontend.
|
||||
// It returns the exit code of the compiler.
|
||||
// We never call os.Exit directly here because it's bad for testing.
|
||||
func Main(args []string) int {
|
||||
if len(args) == 0 {
|
||||
return Help(nil)
|
||||
}
|
||||
|
||||
switch args[0] {
|
||||
case "build":
|
||||
return Build(args[1:])
|
||||
|
||||
case "system":
|
||||
return System(args[1:])
|
||||
|
||||
default:
|
||||
return Help(args[1:])
|
||||
}
|
||||
}
|
37
src/cli/System.go
Normal file
37
src/cli/System.go
Normal file
@ -0,0 +1,37 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"git.akyoto.dev/cli/q/src/log"
|
||||
)
|
||||
|
||||
// System shows system information.
|
||||
func System(args []string) int {
|
||||
line := "%-19s%s\n"
|
||||
|
||||
log.Info.Printf(line, "Platform:", runtime.GOOS)
|
||||
log.Info.Printf(line, "Architecture:", runtime.GOARCH)
|
||||
log.Info.Printf(line, "Go:", runtime.Version())
|
||||
|
||||
// Directory
|
||||
directory, err := os.Getwd()
|
||||
|
||||
if err == nil {
|
||||
log.Info.Printf(line, "Directory:", directory)
|
||||
} else {
|
||||
log.Info.Printf(line, "Directory:", err.Error())
|
||||
}
|
||||
|
||||
// Compiler
|
||||
executable, err := os.Executable()
|
||||
|
||||
if err == nil {
|
||||
log.Info.Printf(line, "Compiler:", executable)
|
||||
} else {
|
||||
log.Info.Printf(line, "Compiler:", err.Error())
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
47
src/cli/cli_test.go
Normal file
47
src/cli/cli_test.go
Normal file
@ -0,0 +1,47 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"git.akyoto.dev/cli/q/src/cli"
|
||||
"git.akyoto.dev/cli/q/src/log"
|
||||
"git.akyoto.dev/go/assert"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
log.Info.SetOutput(io.Discard)
|
||||
log.Error.SetOutput(io.Discard)
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestCLI(t *testing.T) {
|
||||
type cliTest struct {
|
||||
arguments []string
|
||||
expectedExitCode int
|
||||
}
|
||||
|
||||
tests := []cliTest{
|
||||
{[]string{}, 2},
|
||||
{[]string{"invalid"}, 2},
|
||||
{[]string{"system"}, 0},
|
||||
{[]string{"build", "non-existing-directory"}, 1},
|
||||
{[]string{"build", "examples/hello/hello.q"}, 1},
|
||||
{[]string{"build", "examples/hello", "--invalid"}, 2},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
exitCode := cli.Main(test.arguments)
|
||||
t.Log(test.arguments)
|
||||
assert.Equal(t, exitCode, test.expectedExitCode)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBuild(b *testing.B) {
|
||||
args := []string{"build", "examples/hello", "--dry"}
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
cli.Main(args)
|
||||
}
|
||||
}
|
61
src/directory/Walk.go
Normal file
61
src/directory/Walk.go
Normal file
@ -0,0 +1,61 @@
|
||||
package directory
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const blockSize = 8 << 10
|
||||
|
||||
// Walk calls your callback function for every file name inside the directory.
|
||||
// It doesn't distinguish between files and directories.
|
||||
func Walk(directory string, callBack func(string)) {
|
||||
fd, err := syscall.Open(directory, 0, 0)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
defer syscall.Close(fd)
|
||||
buffer := make([]byte, blockSize)
|
||||
|
||||
for {
|
||||
n, err := syscall.ReadDirent(fd, buffer)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if n <= 0 {
|
||||
break
|
||||
}
|
||||
|
||||
readBuffer := buffer[:n]
|
||||
|
||||
for len(readBuffer) > 0 {
|
||||
dirent := (*syscall.Dirent)(unsafe.Pointer(&readBuffer[0]))
|
||||
readBuffer = readBuffer[dirent.Reclen:]
|
||||
|
||||
// Skip deleted files
|
||||
if dirent.Ino == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip hidden files
|
||||
if dirent.Name[0] == '.' {
|
||||
continue
|
||||
}
|
||||
|
||||
for i, c := range dirent.Name {
|
||||
if c != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
bytePointer := (*byte)(unsafe.Pointer(&dirent.Name[0]))
|
||||
name := unsafe.String(bytePointer, i)
|
||||
callBack(name)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
69
src/elf/ELF.go
Normal file
69
src/elf/ELF.go
Normal file
@ -0,0 +1,69 @@
|
||||
package elf
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
)
|
||||
|
||||
const (
|
||||
minAddress = 0x10000
|
||||
baseAddress = 0x40 * minAddress
|
||||
)
|
||||
|
||||
// ELF represents an ELF file.
|
||||
type ELF struct {
|
||||
Header
|
||||
ProgramHeader
|
||||
Code []byte
|
||||
Data []byte
|
||||
}
|
||||
|
||||
// New creates a new ELF binary.
|
||||
func New(code []byte, data []byte) *ELF {
|
||||
elf := &ELF{
|
||||
Header: Header{
|
||||
Magic: [4]byte{0x7F, 'E', 'L', 'F'},
|
||||
Class: 2,
|
||||
Endianness: LittleEndian,
|
||||
Version: 1,
|
||||
OSABI: 0,
|
||||
ABIVersion: 0,
|
||||
Type: TypeExecutable,
|
||||
Architecture: ArchitectureAMD64,
|
||||
FileVersion: 1,
|
||||
EntryPointInMemory: baseAddress + 0x80,
|
||||
ProgramHeaderOffset: HeaderSize,
|
||||
SectionHeaderOffset: 0,
|
||||
Flags: 0,
|
||||
Size: HeaderSize,
|
||||
ProgramHeaderEntrySize: ProgramHeaderSize,
|
||||
ProgramHeaderEntryCount: 1,
|
||||
SectionHeaderEntrySize: SectionHeaderSize,
|
||||
SectionHeaderEntryCount: 0,
|
||||
SectionNameStringTableIndex: 0,
|
||||
},
|
||||
ProgramHeader: ProgramHeader{
|
||||
Type: ProgramTypeLOAD,
|
||||
Flags: ProgramFlagsExecutable,
|
||||
Offset: 0x80,
|
||||
VirtualAddress: baseAddress + 0x80,
|
||||
PhysicalAddress: baseAddress + 0x80,
|
||||
SizeInFile: int64(len(code)),
|
||||
SizeInMemory: int64(len(code)),
|
||||
Align: Align,
|
||||
},
|
||||
Code: code,
|
||||
Data: data,
|
||||
}
|
||||
|
||||
return elf
|
||||
}
|
||||
|
||||
// Write writes the ELF64 format to the given writer.
|
||||
func (elf *ELF) Write(writer io.Writer) {
|
||||
binary.Write(writer, binary.LittleEndian, &elf.Header)
|
||||
binary.Write(writer, binary.LittleEndian, &elf.ProgramHeader)
|
||||
writer.Write([]byte{0, 0, 0, 0, 0, 0, 0, 0})
|
||||
writer.Write(elf.Code)
|
||||
writer.Write(elf.Data)
|
||||
}
|
34
src/elf/Header.go
Normal file
34
src/elf/Header.go
Normal file
@ -0,0 +1,34 @@
|
||||
package elf
|
||||
|
||||
const (
|
||||
LittleEndian = 1
|
||||
TypeExecutable = 2
|
||||
ArchitectureAMD64 = 0x3E
|
||||
HeaderSize = 64
|
||||
CacheLineSize = 64
|
||||
Align = CacheLineSize
|
||||
)
|
||||
|
||||
// Header contains general information.
|
||||
type Header struct {
|
||||
Magic [4]byte
|
||||
Class byte
|
||||
Endianness byte
|
||||
Version byte
|
||||
OSABI byte
|
||||
ABIVersion byte
|
||||
_ [7]byte
|
||||
Type int16
|
||||
Architecture int16
|
||||
FileVersion int32
|
||||
EntryPointInMemory int64
|
||||
ProgramHeaderOffset int64
|
||||
SectionHeaderOffset int64
|
||||
Flags int32
|
||||
Size int16
|
||||
ProgramHeaderEntrySize int16
|
||||
ProgramHeaderEntryCount int16
|
||||
SectionHeaderEntrySize int16
|
||||
SectionHeaderEntryCount int16
|
||||
SectionNameStringTableIndex int16
|
||||
}
|
37
src/elf/ProgramHeader.go
Normal file
37
src/elf/ProgramHeader.go
Normal file
@ -0,0 +1,37 @@
|
||||
package elf
|
||||
|
||||
// ProgramHeaderSize is equal to the size of a program header in bytes.
|
||||
const ProgramHeaderSize = 56
|
||||
|
||||
// ProgramHeader points to the executable part of our program.
|
||||
type ProgramHeader struct {
|
||||
Type ProgramType
|
||||
Flags ProgramFlags
|
||||
Offset int64
|
||||
VirtualAddress int64
|
||||
PhysicalAddress int64
|
||||
SizeInFile int64
|
||||
SizeInMemory int64
|
||||
Align int64
|
||||
}
|
||||
|
||||
type ProgramType int32
|
||||
|
||||
const (
|
||||
ProgramTypeNULL ProgramType = 0
|
||||
ProgramTypeLOAD ProgramType = 1
|
||||
ProgramTypeDYNAMIC ProgramType = 2
|
||||
ProgramTypeINTERP ProgramType = 3
|
||||
ProgramTypeNOTE ProgramType = 4
|
||||
ProgramTypeSHLIB ProgramType = 5
|
||||
ProgramTypePHDR ProgramType = 6
|
||||
ProgramTypeTLS ProgramType = 7
|
||||
)
|
||||
|
||||
type ProgramFlags int32
|
||||
|
||||
const (
|
||||
ProgramFlagsExecutable ProgramFlags = 0x1
|
||||
ProgramFlagsWritable ProgramFlags = 0x2
|
||||
ProgramFlagsReadable ProgramFlags = 0x4
|
||||
)
|
45
src/elf/SectionHeader.go
Normal file
45
src/elf/SectionHeader.go
Normal file
@ -0,0 +1,45 @@
|
||||
package elf
|
||||
|
||||
// SectionHeaderSize is equal to the size of a section header in bytes.
|
||||
const SectionHeaderSize = 64
|
||||
|
||||
// SectionHeader points to the data sections of our program.
|
||||
type SectionHeader struct {
|
||||
NameIndex int32
|
||||
Type SectionType
|
||||
Flags SectionFlags
|
||||
VirtualAddress int64
|
||||
Offset int64
|
||||
SizeInFile int64
|
||||
Link int32
|
||||
Info int32
|
||||
Align int64
|
||||
EntrySize int64
|
||||
}
|
||||
|
||||
type SectionType int32
|
||||
|
||||
const (
|
||||
SectionTypeNULL SectionType = 0
|
||||
SectionTypePROGBITS SectionType = 1
|
||||
SectionTypeSYMTAB SectionType = 2
|
||||
SectionTypeSTRTAB SectionType = 3
|
||||
SectionTypeRELA SectionType = 4
|
||||
SectionTypeHASH SectionType = 5
|
||||
SectionTypeDYNAMIC SectionType = 6
|
||||
SectionTypeNOTE SectionType = 7
|
||||
SectionTypeNOBITS SectionType = 8
|
||||
SectionTypeREL SectionType = 9
|
||||
SectionTypeSHLIB SectionType = 10
|
||||
SectionTypeDYNSYM SectionType = 11
|
||||
)
|
||||
|
||||
type SectionFlags int64
|
||||
|
||||
const (
|
||||
SectionFlagsWritable SectionFlags = 1 << 0
|
||||
SectionFlagsAllocate SectionFlags = 1 << 1
|
||||
SectionFlagsExecutable SectionFlags = 1 << 2
|
||||
SectionFlagsStrings SectionFlags = 1 << 5
|
||||
SectionFlagsTLS SectionFlags = 1 << 10
|
||||
)
|
26
src/elf/elf.md
Normal file
26
src/elf/elf.md
Normal file
@ -0,0 +1,26 @@
|
||||
# ELF
|
||||
|
||||
## Basic structure
|
||||
|
||||
1. ELF header (0x00 - 0x40)
|
||||
2. Program header (0x40 - 0x78)
|
||||
3. Padding (0x78 - 0x80)
|
||||
4. Machine code (0x80)
|
||||
|
||||
## Entry point
|
||||
|
||||
The entry point is defined in the first 64 bytes (ELF header).
|
||||
|
||||
## Base address
|
||||
|
||||
The minimum base address is controlled by the `mmap` settings:
|
||||
|
||||
```shell
|
||||
sysctl vm.mmap_min_addr
|
||||
```
|
||||
|
||||
Usually, this value is 65536 (0x1000).
|
||||
|
||||
## Initialization in Linux
|
||||
|
||||
See `/lib/modules/$(uname -r)/build/arch/x86/include/asm/elf.h`.
|
15
src/errors/InvalidPath.go
Normal file
15
src/errors/InvalidPath.go
Normal file
@ -0,0 +1,15 @@
|
||||
package errors
|
||||
|
||||
import "fmt"
|
||||
|
||||
type InvalidDirectory struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
func (err *InvalidDirectory) Error() string {
|
||||
if err.Path == "" {
|
||||
return "Invalid directory"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("Invalid directory '%s'", err.Path)
|
||||
}
|
14
src/log/log.go
Normal file
14
src/log/log.go
Normal file
@ -0,0 +1,14 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
var (
|
||||
// Info is used for general info messages.
|
||||
Info = log.New(os.Stdout, "", 0)
|
||||
|
||||
// Error is used for error messages.
|
||||
Error = log.New(os.Stderr, "", 0)
|
||||
)
|
Reference in New Issue
Block a user