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

     1  package builder
     2  
     3  import (
     4  	"bytes"
     5  	"debug/elf"
     6  	"debug/macho"
     7  	"encoding/binary"
     8  	"fmt"
     9  	"io"
    10  	"os"
    11  	"runtime"
    12  )
    13  
    14  // ReadBuildID reads the build ID from the currently running executable.
    15  func ReadBuildID() ([]byte, error) {
    16  	executable, err := os.Executable()
    17  	if err != nil {
    18  		return nil, err
    19  	}
    20  	f, err := os.Open(executable)
    21  	if err != nil {
    22  		return nil, err
    23  	}
    24  	defer f.Close()
    25  
    26  	switch runtime.GOOS {
    27  	case "linux", "freebsd", "android":
    28  		// Read the GNU build id section. (Not sure about FreeBSD though...)
    29  		file, err := elf.NewFile(f)
    30  		if err != nil {
    31  			return nil, err
    32  		}
    33  		var gnuID, goID []byte
    34  		for _, section := range file.Sections {
    35  			if section.Type != elf.SHT_NOTE ||
    36  				(section.Name != ".note.gnu.build-id" && section.Name != ".note.go.buildid") {
    37  				continue
    38  			}
    39  			buf := make([]byte, section.Size)
    40  			n, err := section.ReadAt(buf, 0)
    41  			if uint64(n) != section.Size || err != nil {
    42  				return nil, fmt.Errorf("could not read build id: %w", err)
    43  			}
    44  			if section.Name == ".note.gnu.build-id" {
    45  				gnuID = buf
    46  			} else {
    47  				goID = buf
    48  			}
    49  		}
    50  		if gnuID != nil {
    51  			return gnuID, nil
    52  		} else if goID != nil {
    53  			return goID, nil
    54  		}
    55  	case "darwin":
    56  		// Read the LC_UUID load command, which contains the equivalent of a
    57  		// build ID.
    58  		file, err := macho.NewFile(f)
    59  		if err != nil {
    60  			return nil, err
    61  		}
    62  		for _, load := range file.Loads {
    63  			// Unfortunately, the debug/macho package doesn't support the
    64  			// LC_UUID command directly. So we have to read it from
    65  			// macho.LoadBytes.
    66  			load, ok := load.(macho.LoadBytes)
    67  			if !ok {
    68  				continue
    69  			}
    70  			raw := load.Raw()
    71  			command := binary.LittleEndian.Uint32(raw)
    72  			if command != 0x1b {
    73  				// Looking for the LC_UUID load command.
    74  				// LC_UUID is defined here as 0x1b:
    75  				// https://opensource.apple.com/source/xnu/xnu-4570.71.2/EXTERNAL_HEADERS/mach-o/loader.h.auto.html
    76  				continue
    77  			}
    78  			return raw[4:], nil
    79  		}
    80  
    81  		// Normally we would have found a build ID by now. But not on Nix,
    82  		// unfortunately, because Nix adds -no_uuid for some reason:
    83  		// https://github.com/NixOS/nixpkgs/issues/178366
    84  		// Fall back to the same implementation that we use for Windows.
    85  		id, err := readRawGoBuildID(f, 32*1024)
    86  		if len(id) != 0 || err != nil {
    87  			return id, err
    88  		}
    89  	default:
    90  		// On other platforms (such as Windows) there isn't such a convenient
    91  		// build ID. Luckily, Go does have an equivalent of the build ID, which
    92  		// is stored as a special symbol named go.buildid. You can read it
    93  		// using `go tool buildid`, but the code below extracts it directly
    94  		// from the binary.
    95  		// Unfortunately, because of stripping with the -w flag, no symbol
    96  		// table might be available. Therefore, we have to scan the binary
    97  		// directly. Luckily the build ID is always at the start of the file.
    98  		// For details, see:
    99  		// https://github.com/golang/go/blob/master/src/cmd/internal/buildid/buildid.go
   100  		id, err := readRawGoBuildID(f, 4096)
   101  		if len(id) != 0 || err != nil {
   102  			return id, err
   103  		}
   104  	}
   105  	return nil, fmt.Errorf("could not find build ID in %v", executable)
   106  }
   107  
   108  // The Go toolchain stores a build ID in the binary that we can use, as a
   109  // fallback if binary file specific build IDs can't be obtained.
   110  // This function reads that build ID from the binary.
   111  func readRawGoBuildID(f *os.File, prefixSize int) ([]byte, error) {
   112  	fileStart := make([]byte, prefixSize)
   113  	_, err := io.ReadFull(f, fileStart)
   114  	if err != nil {
   115  		return nil, fmt.Errorf("could not read build id from %s: %v", f.Name(), err)
   116  	}
   117  	index := bytes.Index(fileStart, []byte("\xff Go build ID: \""))
   118  	if index < 0 || index > len(fileStart)-103 {
   119  		return nil, fmt.Errorf("could not find build id in %s", f.Name())
   120  	}
   121  	buf := fileStart[index : index+103]
   122  	if bytes.HasPrefix(buf, []byte("\xff Go build ID: \"")) && bytes.HasSuffix(buf, []byte("\"\n \xff")) {
   123  		return buf[len("\xff Go build ID: \"") : len(buf)-1], nil
   124  	}
   125  
   126  	return nil, nil
   127  }