github.com/tinygo-org/tinygo@v0.31.3-0.20240404173401-90b0bf646c27/compileopts/target.go (about)

     1  package compileopts
     2  
     3  // This file loads a target specification from a JSON file.
     4  
     5  import (
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"os"
    11  	"os/exec"
    12  	"path/filepath"
    13  	"reflect"
    14  	"runtime"
    15  	"strings"
    16  
    17  	"github.com/tinygo-org/tinygo/goenv"
    18  )
    19  
    20  // Target specification for a given target. Used for bare metal targets.
    21  //
    22  // The target specification is mostly inspired by Rust:
    23  // https://doc.rust-lang.org/nightly/nightly-rustc/rustc_target/spec/struct.TargetOptions.html
    24  // https://github.com/shepmaster/rust-arduino-blink-led-no-core-with-cargo/blob/master/blink/arduino.json
    25  type TargetSpec struct {
    26  	Inherits         []string `json:"inherits,omitempty"`
    27  	Triple           string   `json:"llvm-target,omitempty"`
    28  	CPU              string   `json:"cpu,omitempty"`
    29  	ABI              string   `json:"target-abi,omitempty"` // rougly equivalent to -mabi= flag
    30  	Features         string   `json:"features,omitempty"`
    31  	GOOS             string   `json:"goos,omitempty"`
    32  	GOARCH           string   `json:"goarch,omitempty"`
    33  	BuildTags        []string `json:"build-tags,omitempty"`
    34  	GC               string   `json:"gc,omitempty"`
    35  	Scheduler        string   `json:"scheduler,omitempty"`
    36  	Serial           string   `json:"serial,omitempty"` // which serial output to use (uart, usb, none)
    37  	Linker           string   `json:"linker,omitempty"`
    38  	RTLib            string   `json:"rtlib,omitempty"` // compiler runtime library (libgcc, compiler-rt)
    39  	Libc             string   `json:"libc,omitempty"`
    40  	AutoStackSize    *bool    `json:"automatic-stack-size,omitempty"` // Determine stack size automatically at compile time.
    41  	DefaultStackSize uint64   `json:"default-stack-size,omitempty"`   // Default stack size if the size couldn't be determined at compile time.
    42  	CFlags           []string `json:"cflags,omitempty"`
    43  	LDFlags          []string `json:"ldflags,omitempty"`
    44  	LinkerScript     string   `json:"linkerscript,omitempty"`
    45  	ExtraFiles       []string `json:"extra-files,omitempty"`
    46  	RP2040BootPatch  *bool    `json:"rp2040-boot-patch,omitempty"` // Patch RP2040 2nd stage bootloader checksum
    47  	Emulator         string   `json:"emulator,omitempty"`
    48  	FlashCommand     string   `json:"flash-command,omitempty"`
    49  	GDB              []string `json:"gdb,omitempty"`
    50  	PortReset        string   `json:"flash-1200-bps-reset,omitempty"`
    51  	SerialPort       []string `json:"serial-port,omitempty"` // serial port IDs in the form "vid:pid"
    52  	FlashMethod      string   `json:"flash-method,omitempty"`
    53  	FlashVolume      []string `json:"msd-volume-name,omitempty"`
    54  	FlashFilename    string   `json:"msd-firmware-name,omitempty"`
    55  	UF2FamilyID      string   `json:"uf2-family-id,omitempty"`
    56  	BinaryFormat     string   `json:"binary-format,omitempty"`
    57  	OpenOCDInterface string   `json:"openocd-interface,omitempty"`
    58  	OpenOCDTarget    string   `json:"openocd-target,omitempty"`
    59  	OpenOCDTransport string   `json:"openocd-transport,omitempty"`
    60  	OpenOCDCommands  []string `json:"openocd-commands,omitempty"`
    61  	OpenOCDVerify    *bool    `json:"openocd-verify,omitempty"` // enable verify when flashing with openocd
    62  	JLinkDevice      string   `json:"jlink-device,omitempty"`
    63  	CodeModel        string   `json:"code-model,omitempty"`
    64  	RelocationModel  string   `json:"relocation-model,omitempty"`
    65  }
    66  
    67  // overrideProperties overrides all properties that are set in child into itself using reflection.
    68  func (spec *TargetSpec) overrideProperties(child *TargetSpec) error {
    69  	specType := reflect.TypeOf(spec).Elem()
    70  	specValue := reflect.ValueOf(spec).Elem()
    71  	childValue := reflect.ValueOf(child).Elem()
    72  
    73  	for i := 0; i < specType.NumField(); i++ {
    74  		field := specType.Field(i)
    75  		src := childValue.Field(i)
    76  		dst := specValue.Field(i)
    77  
    78  		switch kind := field.Type.Kind(); kind {
    79  		case reflect.String: // for strings, just copy the field of child to spec if not empty
    80  			if src.Len() > 0 {
    81  				dst.Set(src)
    82  			}
    83  		case reflect.Uint, reflect.Uint32, reflect.Uint64: // for Uint, copy if not zero
    84  			if src.Uint() != 0 {
    85  				dst.Set(src)
    86  			}
    87  		case reflect.Ptr: // for pointers, copy if not nil
    88  			if !src.IsNil() {
    89  				dst.Set(src)
    90  			}
    91  		case reflect.Slice: // for slices, append the field and check for duplicates
    92  			dst.Set(reflect.AppendSlice(dst, src))
    93  			for i := 0; i < dst.Len(); i++ {
    94  				v := dst.Index(i).String()
    95  				for j := i + 1; j < dst.Len(); j++ {
    96  					w := dst.Index(j).String()
    97  					if v == w {
    98  						return fmt.Errorf("duplicate value '%s' in field %s", v, field.Name)
    99  					}
   100  				}
   101  			}
   102  		default:
   103  			return fmt.Errorf("unknown field type: %s", kind)
   104  		}
   105  	}
   106  	return nil
   107  }
   108  
   109  // load reads a target specification from the JSON in the given io.Reader. It
   110  // may load more targets specified using the "inherits" property.
   111  func (spec *TargetSpec) load(r io.Reader) error {
   112  	err := json.NewDecoder(r).Decode(spec)
   113  	if err != nil {
   114  		return err
   115  	}
   116  
   117  	return nil
   118  }
   119  
   120  // loadFromGivenStr loads the TargetSpec from the given string that could be:
   121  //   - targets/ directory inside the compiler sources
   122  //   - a relative or absolute path to custom (project specific) target specification .json file;
   123  //     the Inherits[] could contain the files from target folder (ex. stm32f4disco)
   124  //     as well as path to custom files (ex. myAwesomeProject.json)
   125  func (spec *TargetSpec) loadFromGivenStr(str string) error {
   126  	path := ""
   127  	if strings.HasSuffix(str, ".json") {
   128  		path, _ = filepath.Abs(str)
   129  	} else {
   130  		path = filepath.Join(goenv.Get("TINYGOROOT"), "targets", strings.ToLower(str)+".json")
   131  	}
   132  	fp, err := os.Open(path)
   133  	if err != nil {
   134  		return err
   135  	}
   136  	defer fp.Close()
   137  	return spec.load(fp)
   138  }
   139  
   140  // resolveInherits loads inherited targets, recursively.
   141  func (spec *TargetSpec) resolveInherits() error {
   142  	// First create a new spec with all the inherited properties.
   143  	newSpec := &TargetSpec{}
   144  	for _, name := range spec.Inherits {
   145  		subtarget := &TargetSpec{}
   146  		err := subtarget.loadFromGivenStr(name)
   147  		if err != nil {
   148  			return err
   149  		}
   150  		err = subtarget.resolveInherits()
   151  		if err != nil {
   152  			return err
   153  		}
   154  		err = newSpec.overrideProperties(subtarget)
   155  		if err != nil {
   156  			return err
   157  		}
   158  	}
   159  
   160  	// When all properties are loaded, make sure they are properly inherited.
   161  	err := newSpec.overrideProperties(spec)
   162  	if err != nil {
   163  		return err
   164  	}
   165  	*spec = *newSpec
   166  
   167  	return nil
   168  }
   169  
   170  // Load a target specification.
   171  func LoadTarget(options *Options) (*TargetSpec, error) {
   172  	if options.Target == "" {
   173  		// Configure based on GOOS/GOARCH environment variables (falling back to
   174  		// runtime.GOOS/runtime.GOARCH), and generate a LLVM target based on it.
   175  		var llvmarch string
   176  		switch options.GOARCH {
   177  		case "386":
   178  			llvmarch = "i386"
   179  		case "amd64":
   180  			llvmarch = "x86_64"
   181  		case "arm64":
   182  			llvmarch = "aarch64"
   183  		case "arm":
   184  			switch options.GOARM {
   185  			case "5":
   186  				llvmarch = "armv5"
   187  			case "6":
   188  				llvmarch = "armv6"
   189  			case "7":
   190  				llvmarch = "armv7"
   191  			default:
   192  				return nil, fmt.Errorf("invalid GOARM=%s, must be 5, 6, or 7", options.GOARM)
   193  			}
   194  		case "wasm":
   195  			llvmarch = "wasm32"
   196  		default:
   197  			llvmarch = options.GOARCH
   198  		}
   199  		llvmvendor := "unknown"
   200  		llvmos := options.GOOS
   201  		switch llvmos {
   202  		case "darwin":
   203  			// Use macosx* instead of darwin, otherwise darwin/arm64 will refer
   204  			// to iOS!
   205  			llvmos = "macosx10.12.0"
   206  			if llvmarch == "aarch64" {
   207  				// Looks like Apple prefers to call this architecture ARM64
   208  				// instead of AArch64.
   209  				llvmarch = "arm64"
   210  				llvmos = "macosx11.0.0"
   211  			}
   212  			llvmvendor = "apple"
   213  		case "wasip1":
   214  			llvmos = "wasi"
   215  		}
   216  		// Target triples (which actually have four components, but are called
   217  		// triples for historical reasons) have the form:
   218  		//   arch-vendor-os-environment
   219  		target := llvmarch + "-" + llvmvendor + "-" + llvmos
   220  		if options.GOOS == "windows" {
   221  			target += "-gnu"
   222  		} else if options.GOARCH == "arm" {
   223  			target += "-gnueabihf"
   224  		}
   225  		return defaultTarget(options.GOOS, options.GOARCH, target)
   226  	}
   227  
   228  	// See whether there is a target specification for this target (e.g.
   229  	// Arduino).
   230  	spec := &TargetSpec{}
   231  	err := spec.loadFromGivenStr(options.Target)
   232  	if err != nil {
   233  		return nil, err
   234  	}
   235  	// Successfully loaded this target from a built-in .json file. Make sure
   236  	// it includes all parents as specified in the "inherits" key.
   237  	err = spec.resolveInherits()
   238  	if err != nil {
   239  		return nil, fmt.Errorf("%s : %w", options.Target, err)
   240  	}
   241  
   242  	if spec.Scheduler == "asyncify" {
   243  		spec.ExtraFiles = append(spec.ExtraFiles, "src/internal/task/task_asyncify_wasm.S")
   244  	}
   245  
   246  	return spec, nil
   247  }
   248  
   249  // GetTargetSpecs retrieves target specifications from the TINYGOROOT targets
   250  // directory.  Only valid target JSON files are considered, and the function
   251  // returns a map of target names to their respective TargetSpec.
   252  func GetTargetSpecs() (map[string]*TargetSpec, error) {
   253  	dir := filepath.Join(goenv.Get("TINYGOROOT"), "targets")
   254  	entries, err := os.ReadDir(dir)
   255  	if err != nil {
   256  		return nil, fmt.Errorf("could not list targets: %w", err)
   257  	}
   258  
   259  	maps := map[string]*TargetSpec{}
   260  	for _, entry := range entries {
   261  		entryInfo, err := entry.Info()
   262  		if err != nil {
   263  			return nil, fmt.Errorf("could not get entry info: %w", err)
   264  		}
   265  		if !entryInfo.Mode().IsRegular() || !strings.HasSuffix(entry.Name(), ".json") {
   266  			// Only inspect JSON files.
   267  			continue
   268  		}
   269  		path := filepath.Join(dir, entry.Name())
   270  		spec, err := LoadTarget(&Options{Target: path})
   271  		if err != nil {
   272  			return nil, fmt.Errorf("could not list target: %w", err)
   273  		}
   274  		if spec.FlashMethod == "" && spec.FlashCommand == "" && spec.Emulator == "" {
   275  			// This doesn't look like a regular target file, but rather like
   276  			// a parent target (such as targets/cortex-m.json).
   277  			continue
   278  		}
   279  		name := entry.Name()
   280  		name = name[:len(name)-5]
   281  		maps[name] = spec
   282  	}
   283  	return maps, nil
   284  }
   285  
   286  func defaultTarget(goos, goarch, triple string) (*TargetSpec, error) {
   287  	// No target spec available. Use the default one, useful on most systems
   288  	// with a regular OS.
   289  	spec := TargetSpec{
   290  		Triple:           triple,
   291  		GOOS:             goos,
   292  		GOARCH:           goarch,
   293  		BuildTags:        []string{goos, goarch},
   294  		GC:               "precise",
   295  		Scheduler:        "tasks",
   296  		Linker:           "cc",
   297  		DefaultStackSize: 1024 * 64, // 64kB
   298  		GDB:              []string{"gdb"},
   299  		PortReset:        "false",
   300  	}
   301  	switch goarch {
   302  	case "386":
   303  		spec.CPU = "pentium4"
   304  		spec.Features = "+cmov,+cx8,+fxsr,+mmx,+sse,+sse2,+x87"
   305  	case "amd64":
   306  		spec.CPU = "x86-64"
   307  		spec.Features = "+cmov,+cx8,+fxsr,+mmx,+sse,+sse2,+x87"
   308  	case "arm":
   309  		spec.CPU = "generic"
   310  		spec.CFlags = append(spec.CFlags, "-fno-unwind-tables", "-fno-asynchronous-unwind-tables")
   311  		switch strings.Split(triple, "-")[0] {
   312  		case "armv5":
   313  			spec.Features = "+armv5t,+strict-align,-aes,-bf16,-d32,-dotprod,-fp-armv8,-fp-armv8d16,-fp-armv8d16sp,-fp-armv8sp,-fp16,-fp16fml,-fp64,-fpregs,-fullfp16,-mve.fp,-neon,-sha2,-thumb-mode,-vfp2,-vfp2sp,-vfp3,-vfp3d16,-vfp3d16sp,-vfp3sp,-vfp4,-vfp4d16,-vfp4d16sp,-vfp4sp"
   314  		case "armv6":
   315  			spec.Features = "+armv6,+dsp,+fp64,+strict-align,+vfp2,+vfp2sp,-aes,-d32,-fp-armv8,-fp-armv8d16,-fp-armv8d16sp,-fp-armv8sp,-fp16,-fp16fml,-fullfp16,-neon,-sha2,-thumb-mode,-vfp3,-vfp3d16,-vfp3d16sp,-vfp3sp,-vfp4,-vfp4d16,-vfp4d16sp,-vfp4sp"
   316  		case "armv7":
   317  			spec.Features = "+armv7-a,+d32,+dsp,+fp64,+neon,+vfp2,+vfp2sp,+vfp3,+vfp3d16,+vfp3d16sp,+vfp3sp,-aes,-fp-armv8,-fp-armv8d16,-fp-armv8d16sp,-fp-armv8sp,-fp16,-fp16fml,-fullfp16,-sha2,-thumb-mode,-vfp4,-vfp4d16,-vfp4d16sp,-vfp4sp"
   318  		}
   319  	case "arm64":
   320  		spec.CPU = "generic"
   321  		if goos == "darwin" {
   322  			spec.Features = "+neon"
   323  		} else if goos == "windows" {
   324  			spec.Features = "+neon,-fmv"
   325  		} else { // linux
   326  			spec.Features = "+neon,-fmv,-outline-atomics"
   327  		}
   328  	case "wasm":
   329  		spec.CPU = "generic"
   330  		spec.Features = "+bulk-memory,+mutable-globals,+nontrapping-fptoint,+sign-ext"
   331  		spec.BuildTags = append(spec.BuildTags, "tinygo.wasm")
   332  		spec.CFlags = append(spec.CFlags,
   333  			"-mbulk-memory",
   334  			"-mnontrapping-fptoint",
   335  			"-msign-ext",
   336  		)
   337  	}
   338  	if goos == "darwin" {
   339  		spec.Linker = "ld.lld"
   340  		spec.Libc = "darwin-libSystem"
   341  		arch := strings.Split(triple, "-")[0]
   342  		platformVersion := strings.TrimPrefix(strings.Split(triple, "-")[2], "macosx")
   343  		spec.LDFlags = append(spec.LDFlags,
   344  			"-flavor", "darwin",
   345  			"-dead_strip",
   346  			"-arch", arch,
   347  			"-platform_version", "macos", platformVersion, platformVersion,
   348  		)
   349  	} else if goos == "linux" {
   350  		spec.Linker = "ld.lld"
   351  		spec.RTLib = "compiler-rt"
   352  		spec.Libc = "musl"
   353  		spec.LDFlags = append(spec.LDFlags, "--gc-sections")
   354  		if goarch == "arm64" {
   355  			// Disable outline atomics. For details, see:
   356  			// https://cpufun.substack.com/p/atomics-in-aarch64
   357  			// A better way would be to fully support outline atomics, which
   358  			// makes atomics slightly more efficient on systems with many cores.
   359  			// But the instructions are only supported on newer aarch64 CPUs, so
   360  			// this feature is normally put in a system library which does
   361  			// feature detection for you.
   362  			// We take the lazy way out and simply disable this feature, instead
   363  			// of enabling it in compiler-rt (which is a bit more complicated).
   364  			// We don't really need this feature anyway as we don't even support
   365  			// proper threading.
   366  			spec.CFlags = append(spec.CFlags, "-mno-outline-atomics")
   367  		}
   368  	} else if goos == "windows" {
   369  		spec.Linker = "ld.lld"
   370  		spec.Libc = "mingw-w64"
   371  		// Note: using a medium code model, low image base and no ASLR
   372  		// because Go doesn't really need those features. ASLR patches
   373  		// around issues for unsafe languages like C/C++ that are not
   374  		// normally present in Go (without explicitly opting in).
   375  		// For more discussion:
   376  		// https://groups.google.com/g/Golang-nuts/c/Jd9tlNc6jUE/m/Zo-7zIP_m3MJ?pli=1
   377  		switch goarch {
   378  		case "amd64":
   379  			spec.LDFlags = append(spec.LDFlags,
   380  				"-m", "i386pep",
   381  				"--image-base", "0x400000",
   382  			)
   383  		case "arm64":
   384  			spec.LDFlags = append(spec.LDFlags,
   385  				"-m", "arm64pe",
   386  			)
   387  		}
   388  		spec.LDFlags = append(spec.LDFlags,
   389  			"-Bdynamic",
   390  			"--gc-sections",
   391  			"--no-insert-timestamp",
   392  			"--no-dynamicbase",
   393  		)
   394  	} else if goos == "wasip1" {
   395  		spec.GC = "" // use default GC
   396  		spec.Scheduler = "asyncify"
   397  		spec.Linker = "wasm-ld"
   398  		spec.RTLib = "compiler-rt"
   399  		spec.Libc = "wasi-libc"
   400  		spec.DefaultStackSize = 1024 * 64 // 64kB
   401  		spec.LDFlags = append(spec.LDFlags,
   402  			"--stack-first",
   403  			"--no-demangle",
   404  		)
   405  		spec.Emulator = "wasmtime --dir={tmpDir}::/tmp {}"
   406  		spec.ExtraFiles = append(spec.ExtraFiles,
   407  			"src/runtime/asm_tinygowasm.S",
   408  			"src/internal/task/task_asyncify_wasm.S",
   409  		)
   410  	} else {
   411  		spec.LDFlags = append(spec.LDFlags, "-no-pie", "-Wl,--gc-sections") // WARNING: clang < 5.0 requires -nopie
   412  	}
   413  	if goarch != "wasm" {
   414  		suffix := ""
   415  		if goos == "windows" && goarch == "amd64" {
   416  			// Windows uses a different calling convention on amd64 from other
   417  			// operating systems so we need separate assembly files.
   418  			suffix = "_windows"
   419  		}
   420  		spec.ExtraFiles = append(spec.ExtraFiles, "src/runtime/asm_"+goarch+suffix+".S")
   421  		spec.ExtraFiles = append(spec.ExtraFiles, "src/internal/task/task_stack_"+goarch+suffix+".S")
   422  	}
   423  	if goarch != runtime.GOARCH {
   424  		// Some educated guesses as to how to invoke helper programs.
   425  		spec.GDB = []string{"gdb-multiarch"}
   426  		if goos == "linux" {
   427  			switch goarch {
   428  			case "386":
   429  				// amd64 can _usually_ run 32-bit programs, so skip the emulator in that case.
   430  				if runtime.GOARCH != "amd64" {
   431  					spec.Emulator = "qemu-i386 {}"
   432  				}
   433  			case "amd64":
   434  				spec.Emulator = "qemu-x86_64 {}"
   435  			case "arm":
   436  				spec.Emulator = "qemu-arm {}"
   437  			case "arm64":
   438  				spec.Emulator = "qemu-aarch64 {}"
   439  			}
   440  		}
   441  	}
   442  	if goos != runtime.GOOS {
   443  		if goos == "windows" {
   444  			spec.Emulator = "wine {}"
   445  		}
   446  	}
   447  	return &spec, nil
   448  }
   449  
   450  // LookupGDB looks up a gdb executable.
   451  func (spec *TargetSpec) LookupGDB() (string, error) {
   452  	if len(spec.GDB) == 0 {
   453  		return "", errors.New("gdb not configured in the target specification")
   454  	}
   455  	for _, d := range spec.GDB {
   456  		_, err := exec.LookPath(d)
   457  		if err == nil {
   458  			return d, nil
   459  		}
   460  	}
   461  	return "", errors.New("no gdb found configured in the target specification (" + strings.Join(spec.GDB, ", ") + ")")
   462  }