github.com/mutagen-io/mutagen@v0.18.0-rc1/scripts/build.go (about)

     1  package main
     2  
     3  import (
     4  	"archive/tar"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"log"
     9  	"os"
    10  	"os/exec"
    11  	"path/filepath"
    12  	"runtime"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/spf13/pflag"
    17  
    18  	"github.com/klauspost/compress/gzip"
    19  
    20  	"github.com/mutagen-io/mutagen/cmd"
    21  
    22  	"github.com/mutagen-io/mutagen/pkg/agent"
    23  	"github.com/mutagen-io/mutagen/pkg/mutagen"
    24  )
    25  
    26  const (
    27  	// agentPackage is the Go package URL to use for building Mutagen agent
    28  	// binaries.
    29  	agentPackage = "github.com/mutagen-io/mutagen/cmd/mutagen-agent"
    30  	// cliPackage is the Go package URL to use for building Mutagen binaries.
    31  	cliPackage = "github.com/mutagen-io/mutagen/cmd/mutagen"
    32  
    33  	// agentBuildSubdirectoryName is the name of the build subdirectory where
    34  	// agent binaries are built.
    35  	agentBuildSubdirectoryName = "agent"
    36  	// cliBuildSubdirectoryName is the name of the build subdirectory where CLI
    37  	// binaries are built.
    38  	cliBuildSubdirectoryName = "cli"
    39  	// releaseBuildSubdirectoryName is the name of the build subdirectory where
    40  	// release bundles are built.
    41  	releaseBuildSubdirectoryName = "release"
    42  
    43  	// agentBaseName is the name of the Mutagen agent binary without any path or
    44  	// extension.
    45  	agentBaseName = "mutagen-agent"
    46  	// cliBaseName is the name of the Mutagen binary without any path or
    47  	// extension.
    48  	cliBaseName = "mutagen"
    49  
    50  	// minimumMacOSVersion is the minimum version of macOS that we'll support
    51  	// (currently pinned to the oldest version of macOS that Mutagen's minimum
    52  	// Go version supports).
    53  	minimumMacOSVersion = "10.15"
    54  
    55  	// minimumARMSupport is the value to pass to the GOARM environment variable
    56  	// when building binaries. We currently specify support for ARMv5. This will
    57  	// enable software-based floating point. For our use case, this is totally
    58  	// fine, because we don't have any floating-point-heavy code, and the
    59  	// resulting binary bloat is very minimal. This won't apply for arm64, which
    60  	// always has hardware-based floating point support. For more information,
    61  	// see: https://github.com/golang/go/wiki/GoArm
    62  	minimumARMSupport = "5"
    63  )
    64  
    65  // Target specifies a GOOS/GOARCH combination.
    66  type Target struct {
    67  	// GOOS is the GOOS environment variable specification for the target.
    68  	GOOS string
    69  	// GOARCH is the GOARCH environment variable specification for the target.
    70  	GOARCH string
    71  }
    72  
    73  // String generates a human-readable representation of the target.
    74  func (t Target) String() string {
    75  	return fmt.Sprintf("%s/%s", t.GOOS, t.GOARCH)
    76  }
    77  
    78  // Name generates a representation of the target that is suitable for paths and
    79  // file names.
    80  func (t Target) Name() string {
    81  	return fmt.Sprintf("%s_%s", t.GOOS, t.GOARCH)
    82  }
    83  
    84  // ExecutableName formats executable names for the target.
    85  func (t Target) ExecutableName(base string) string {
    86  	// If we're on Windows, append a ".exe" extension.
    87  	if t.GOOS == "windows" {
    88  		return fmt.Sprintf("%s.exe", base)
    89  	}
    90  
    91  	// Otherwise return the base name unmodified.
    92  	return base
    93  }
    94  
    95  // appendGoEnv modifies an environment specification to make the Go toolchain
    96  // generate output for the target. It assumes that the resulting environment
    97  // will be used with os/exec.Cmd and thus doesn't avoid duplicate variables.
    98  func (t Target) appendGoEnv(environment []string) []string {
    99  	// Override GOOS/GOARCH.
   100  	environment = append(environment, fmt.Sprintf("GOOS=%s", t.GOOS))
   101  	environment = append(environment, fmt.Sprintf("GOARCH=%s", t.GOARCH))
   102  
   103  	// If we're building a macOS binary on macOS, then we enable cgo because
   104  	// we'll need it to access the FSEvents API. We have to enable it explicitly
   105  	// because Go won't enable it when cross compiling between different Darwin
   106  	// architectures. We also need to tell the C compiler and external linker to
   107  	// support older versions of macOS. These flags will tell the C compiler to
   108  	// generate code compatible with the target version of macOS and tell the
   109  	// external linker what value to embed for the LC_VERSION_MIN_MACOSX flag in
   110  	// the resulting Mach-O binaries. Go's internal linker automatically
   111  	// defaults to a relatively liberal (old) value for this flag, but since
   112  	// we're using an external linker, it defaults to the current SDK version.
   113  	//
   114  	// For all other platforms, we disable cgo. This is essential for our Linux
   115  	// CI setup, because we build agent executables during testing that we then
   116  	// run inside Docker containers for our integration tests. These containers
   117  	// typically run Alpine Linux, and if the agent binary is linked to C
   118  	// libraries that only exist on the build system, then they won't work
   119  	// inside the container. We can't disable cgo on a global basis though,
   120  	// because it's needed for race condition testing. Another reason that it's
   121  	// good to disable cgo when building agent binaries during testing is that
   122  	// the release agent binaries will also have cgo disabled (except on macOS),
   123  	// and we'll want to faithfully recreate that.
   124  	if t.GOOS == "darwin" && runtime.GOOS == "darwin" {
   125  		environment = append(environment, "CGO_ENABLED=1")
   126  		environment = append(environment, fmt.Sprintf("CGO_CFLAGS=-mmacosx-version-min=%s", minimumMacOSVersion))
   127  		environment = append(environment, fmt.Sprintf("CGO_LDFLAGS=-mmacosx-version-min=%s", minimumMacOSVersion))
   128  	} else {
   129  		environment = append(environment, "CGO_ENABLED=0")
   130  	}
   131  
   132  	// Set up ARM target support. See notes for definition of minimumARMSupport.
   133  	// We don't need to unset any existing GOARM variables since they simply
   134  	// won't be used if we're not targeting (non-64-bit) ARM systems.
   135  	if t.GOARCH == "arm" {
   136  		environment = append(environment, fmt.Sprintf("GOARM=%s", minimumARMSupport))
   137  	}
   138  
   139  	// Done.
   140  	return environment
   141  }
   142  
   143  // IsCrossTarget determines whether or not the target represents a
   144  // cross-compilation target (i.e. not the native target for the current Go
   145  // toolchain).
   146  func (t Target) IsCrossTarget() bool {
   147  	return t.GOOS != runtime.GOOS || t.GOARCH != runtime.GOARCH
   148  }
   149  
   150  // IncludeAgentInSlimBuildModes indicates whether or not the target should have
   151  // an agent binary included in the agent bundle in slim and release-slim modes.
   152  func (t Target) IncludeAgentInSlimBuildModes() bool {
   153  	return !t.IsCrossTarget() ||
   154  		(t.GOOS == "darwin") ||
   155  		(t.GOOS == "windows" && t.GOARCH == "amd64") ||
   156  		(t.GOOS == "linux" && (t.GOARCH == "amd64" || t.GOARCH == "arm64")) ||
   157  		(t.GOOS == "freebsd" && t.GOARCH == "amd64")
   158  }
   159  
   160  // BuildBundleInReleaseSlimMode indicates whether or not the target should have
   161  // a release bundle built in release-slim mode.
   162  func (t Target) BuildBundleInReleaseSlimMode() bool {
   163  	return !t.IsCrossTarget() ||
   164  		(t.GOOS == "darwin") ||
   165  		(t.GOOS == "windows" && t.GOARCH == "amd64") ||
   166  		(t.GOOS == "linux" && t.GOARCH == "amd64")
   167  }
   168  
   169  // Build executes a module-aware build of the specified package URL, storing the
   170  // output of the build at the specified path.
   171  func (t Target) Build(url, output string, enableSSPLEnhancements, disableDebug bool) error {
   172  	// Compute the build command. If we don't need debugging, then we use the -s
   173  	// and -w linker flags to omit the symbol table and debugging information.
   174  	// This shaves off about 25% of the binary size and only disables debugging
   175  	// (stack traces are still intact). For more information, see:
   176  	// https://blog.filippo.io/shrink-your-go-binaries-with-this-one-weird-trick
   177  	// In this case, we also trim the code paths stored in the executable, as
   178  	// there's no use in having the full paths available.
   179  	arguments := []string{"build", "-o", output}
   180  	var tags []string
   181  	if url == cliPackage {
   182  		tags = append(tags, "mutagencli")
   183  	}
   184  	if url == agentPackage {
   185  		tags = append(tags, "mutagenagent")
   186  	}
   187  	if enableSSPLEnhancements {
   188  		tags = append(tags, "mutagensspl")
   189  	}
   190  	if len(tags) > 0 {
   191  		arguments = append(arguments, "-tags", strings.Join(tags, ","))
   192  	}
   193  	if disableDebug {
   194  		arguments = append(arguments, "-ldflags=-s -w", "-trimpath")
   195  	}
   196  	arguments = append(arguments, url)
   197  
   198  	// Create the build command.
   199  	builder := exec.Command("go", arguments...)
   200  
   201  	// Set the environment.
   202  	builder.Env = t.appendGoEnv(builder.Environ())
   203  
   204  	// Forward input, output, and error streams.
   205  	builder.Stdin = os.Stdin
   206  	builder.Stdout = os.Stdout
   207  	builder.Stderr = os.Stderr
   208  
   209  	// Run the build.
   210  	return builder.Run()
   211  }
   212  
   213  // targets encodes which combinations of GOOS and GOARCH we want to use for
   214  // building agent and CLI binaries. We don't build every target at the moment,
   215  // but we do list all potential targets here and comment out those we don't
   216  // support. This list is created from https://golang.org/doc/install/source.
   217  // Unfortunately there's no automated way to construct this list, but that's
   218  // fine since we have to manually groom it anyway.
   219  var targets = []Target{
   220  	// Define AIX targets.
   221  	{"aix", "ppc64"},
   222  
   223  	// Define Android targets. We disable support for Android since it doesn't
   224  	// have a clearly defined use case as a target platform, though there might
   225  	// be certain development scenarios where it would make sense as an endpoint
   226  	// (via a third-party SSH server on the device).
   227  	// {"android", "386"},
   228  	// {"android", "amd64"},
   229  	// {"android", "arm"},
   230  	// {"android", "arm64"},
   231  
   232  	// Define macOS targets.
   233  	{"darwin", "amd64"},
   234  	{"darwin", "arm64"},
   235  
   236  	// Define DragonFlyBSD targets.
   237  	{"dragonfly", "amd64"},
   238  
   239  	// Define FreeBSD targets.
   240  	{"freebsd", "386"},
   241  	{"freebsd", "amd64"},
   242  	{"freebsd", "arm"},
   243  	// TODO: The freebsd/arm64 port was added in Go 1.14, but for some reason
   244  	// isn't documented at https://golang.org/doc/install/source. Submit a pull
   245  	// request to add it to the Go documentation.
   246  	{"freebsd", "arm64"},
   247  
   248  	// Define illumos targets. We disable explicit support for illumos because
   249  	// it's already effectively supported by our Solaris target. illumos is (at
   250  	// least for Mutagen's purposes) an ABI-compatible superset of Solaris, so
   251  	// there's no need for a separate build. Within the Go toolchain, runtime,
   252  	// and standard library, most of illumos' support is provided by the Solaris
   253  	// port. The "illumos" target even implies the "solaris" build constraint.
   254  	// As such, the Solaris binaries should work fine for illumos distributions.
   255  	// Also, the uname command on illumos returns the same kernel name ("SunOS")
   256  	// as Solaris, so our probing wouldn't be able to identify illumos anyway.
   257  	// {"illumos", "amd64"},
   258  
   259  	// Define WebAssembly targets. We disable support for WebAssembly since it
   260  	// doesn't make sense as a target platform.
   261  	// {"js", "wasm"},
   262  
   263  	// Define iOS/iPadOS/watchOS/tvOS targets. We disable support for these
   264  	// since they don't make sense as target platforms.
   265  	// TODO: The ios/amd64 port was added in Go 1.16, but for some reason isn't
   266  	// documented at https://golang.org/doc/install/source. Submit a pull
   267  	// request to add it to the Go documentation.
   268  	// {"ios", "amd64"},
   269  	// {"ios", "arm64"},
   270  
   271  	// Define Linux targets.
   272  	{"linux", "386"},
   273  	{"linux", "amd64"},
   274  	{"linux", "arm"},
   275  	{"linux", "arm64"},
   276  	// TODO: Assess whether or not we want to support LoongArch. Support was
   277  	// added in Go 1.19, but it sounds like most real-world deployments use a
   278  	// Linux kernel that's too old to support binaries compiled by the official
   279  	// Go toolchain. If this situation changes, then it's certainly worth
   280  	// enabling support. The code does build successfully on this architecture.
   281  	// In this case, we'll also need to update platform detection with the
   282  	// appropriate uname -m value.
   283  	// {"linux", "loong64"},
   284  	{"linux", "ppc64"},
   285  	{"linux", "ppc64le"},
   286  	{"linux", "mips"},
   287  	{"linux", "mipsle"},
   288  	{"linux", "mips64"},
   289  	{"linux", "mips64le"},
   290  	{"linux", "riscv64"},
   291  	{"linux", "s390x"},
   292  
   293  	// Define NetBSD targets.
   294  	{"netbsd", "386"},
   295  	{"netbsd", "amd64"},
   296  	{"netbsd", "arm"},
   297  	// TODO: The netbsd/arm64 port was added in Go 1.16, but for some reason
   298  	// isn't documented at https://golang.org/doc/install/source. Submit a pull
   299  	// request to add it to the Go documentation.
   300  	{"netbsd", "arm64"},
   301  
   302  	// Define OpenBSD targets.
   303  	{"openbsd", "386"},
   304  	{"openbsd", "amd64"},
   305  	{"openbsd", "arm"},
   306  	{"openbsd", "arm64"},
   307  	// TODO: The openbsd/mips64 port was added in Go 1.16, but for some reason
   308  	// isn't documented at https://golang.org/doc/install/source. Submit a pull
   309  	// request to add it to the Go documentation.
   310  	// TODO: The openbsd/mips64 port seems to be broken when using the Go sys
   311  	// subrepository after v0.1.0 - the Go linker crashes with a segfault. The
   312  	// port also doesn't seem to have been tested on the Go build dashboard for
   313  	// quite some time, so its reliability at this point is suspect. Until the
   314  	// picture there clarifies a bit, it's not worth letting this one port hold
   315  	// back the others from receiving updates.
   316  	// {"openbsd", "mips64"},
   317  
   318  	// Define Plan 9 targets. We disable support for Plan 9 because it's missing
   319  	// too many system calls and other APIs necessary for Mutagen to build. It
   320  	// might make sense to support Plan 9 as an endpoint for certain development
   321  	// scenarios, but it will take a significant amount of work just to get the
   322  	// Mutagen agent to build.
   323  	// {"plan9", "386"},
   324  	// {"plan9", "amd64"},
   325  	// {"plan9", "arm"},
   326  
   327  	// Define Solaris targets.
   328  	{"solaris", "amd64"},
   329  
   330  	// Define Windows targets.
   331  	{"windows", "386"},
   332  	{"windows", "amd64"},
   333  	// TODO: The windows/arm port was added in Go 1.12, but for some reason
   334  	// isn't documented at https://golang.org/doc/install/source. Submit a pull
   335  	// request to add it to the Go documentation.
   336  	{"windows", "arm"},
   337  	{"windows", "arm64"},
   338  }
   339  
   340  // macOSCodeSign performs macOS code signing on the specified path using the
   341  // specified signing identity. It performs code signing in a manner suitable for
   342  // later submission to Apple for notarization.
   343  func macOSCodeSign(path, identity string) error {
   344  	// Create the code signing command.
   345  	//
   346  	// We include the --force flag because the Go toolchain won't touch binaries
   347  	// if they don't need to be rebuilt and thus we might have a signature from
   348  	// a previous build. In that case, the code signing operation will fail
   349  	// without --force. When --force is specified, any existing signature will
   350  	// be overwritten, unless it's using the same code signing identity, in
   351  	// which case it will simply be left in place (which is actually optimal for
   352  	// for repeated local usage). Note that the --force flag is not required to
   353  	// override ad hoc signatures (which the Go toolchain will add by default
   354  	// darwin/arm64 binaries).
   355  	//
   356  	// The --options runtime and --timestamp flags are required to enable the
   357  	// hardened runtime (which doesn't affect Mutagen binaries) and to use a
   358  	// secure signing timestamp, both of which are required for notarization.
   359  	codesign := exec.Command("codesign",
   360  		"--sign", identity,
   361  		"--force",
   362  		"--options", "runtime",
   363  		"--timestamp",
   364  		"--verbose",
   365  		path,
   366  	)
   367  
   368  	// Forward input, output, and error streams.
   369  	codesign.Stdin = os.Stdin
   370  	codesign.Stdout = os.Stdout
   371  	codesign.Stderr = os.Stderr
   372  
   373  	// Run code signing.
   374  	return codesign.Run()
   375  }
   376  
   377  // archiveBuilderCopyBufferSize determines the size of the copy buffer used when
   378  // generating archive files.
   379  // TODO: Figure out if we should set this on a per-machine basis. This value is
   380  // taken from Go's io.Copy method, which defaults to allocating a 32k buffer if
   381  // none is provided.
   382  const archiveBuilderCopyBufferSize = 32 * 1024
   383  
   384  type ArchiveBuilder struct {
   385  	file       *os.File
   386  	compressor *gzip.Writer
   387  	archiver   *tar.Writer
   388  	copyBuffer []byte
   389  }
   390  
   391  func NewArchiveBuilder(bundlePath string) (*ArchiveBuilder, error) {
   392  	// Open the underlying file.
   393  	file, err := os.Create(bundlePath)
   394  	if err != nil {
   395  		return nil, fmt.Errorf("unable to create target file: %w", err)
   396  	}
   397  
   398  	// Create the compressor.
   399  	compressor, err := gzip.NewWriterLevel(file, gzip.BestCompression)
   400  	if err != nil {
   401  		file.Close()
   402  		return nil, fmt.Errorf("unable to create compressor: %w", err)
   403  	}
   404  
   405  	// Success.
   406  	return &ArchiveBuilder{
   407  		file:       file,
   408  		compressor: compressor,
   409  		archiver:   tar.NewWriter(compressor),
   410  		copyBuffer: make([]byte, archiveBuilderCopyBufferSize),
   411  	}, nil
   412  }
   413  
   414  func (b *ArchiveBuilder) Close() error {
   415  	// Close in the necessary order to trigger flushes.
   416  	if err := b.archiver.Close(); err != nil {
   417  		b.compressor.Close()
   418  		b.file.Close()
   419  		return fmt.Errorf("unable to close archiver: %w", err)
   420  	} else if err := b.compressor.Close(); err != nil {
   421  		b.file.Close()
   422  		return fmt.Errorf("unable to close compressor: %w", err)
   423  	} else if err := b.file.Close(); err != nil {
   424  		return fmt.Errorf("unable to close file: %w", err)
   425  	}
   426  
   427  	// Success.
   428  	return nil
   429  }
   430  
   431  func (b *ArchiveBuilder) Add(name, path string, mode int64) error {
   432  	// If the name is empty, use the base name.
   433  	if name == "" {
   434  		name = filepath.Base(path)
   435  	}
   436  
   437  	// Open the file and ensure its cleanup.
   438  	file, err := os.Open(path)
   439  	if err != nil {
   440  		return fmt.Errorf("unable to open file: %w", err)
   441  	}
   442  	defer file.Close()
   443  
   444  	// Compute its size.
   445  	stat, err := file.Stat()
   446  	if err != nil {
   447  		return fmt.Errorf("unable to determine file size: %w", err)
   448  	}
   449  	size := stat.Size()
   450  
   451  	// Write the header for the entry.
   452  	header := &tar.Header{
   453  		Name:    name,
   454  		Mode:    mode,
   455  		Size:    size,
   456  		ModTime: time.Now(),
   457  	}
   458  	if err := b.archiver.WriteHeader(header); err != nil {
   459  		return fmt.Errorf("unable to write archive header: %w", err)
   460  	}
   461  
   462  	// Copy the file contents.
   463  	if _, err := io.CopyBuffer(b.archiver, file, b.copyBuffer); err != nil {
   464  		return fmt.Errorf("unable to write archive entry: %w", err)
   465  	}
   466  
   467  	// Success.
   468  	return nil
   469  }
   470  
   471  // copyFile copies the contents at sourcePath to a newly created file at
   472  // destinationPath that inherits the permissions of sourcePath.
   473  func copyFile(sourcePath, destinationPath string) error {
   474  	// Open the source file and defer its closure.
   475  	source, err := os.Open(sourcePath)
   476  	if err != nil {
   477  		return fmt.Errorf("unable to open source file: %w", err)
   478  	}
   479  	defer source.Close()
   480  
   481  	// Grab source file metadata.
   482  	metadata, err := source.Stat()
   483  	if err != nil {
   484  		return fmt.Errorf("unable to query source file metadata: %w", err)
   485  	}
   486  
   487  	// Remove the destination.
   488  	os.Remove(destinationPath)
   489  
   490  	// Create the destination file and defer its closure. We open with exclusive
   491  	// creation flags to ensure that we're the ones creating the file so that
   492  	// its permissions are set correctly.
   493  	destination, err := os.OpenFile(destinationPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, metadata.Mode()&os.ModePerm)
   494  	if err != nil {
   495  		return fmt.Errorf("unable to create destination file: %w", err)
   496  	}
   497  	defer destination.Close()
   498  
   499  	// Copy contents.
   500  	if count, err := io.Copy(destination, source); err != nil {
   501  		return fmt.Errorf("unable to copy data: %w", err)
   502  	} else if count != metadata.Size() {
   503  		return errors.New("copied size does not match expected")
   504  	}
   505  
   506  	// Success.
   507  	return nil
   508  }
   509  
   510  var usage = `usage: build [-h|--help] [-m|--mode=<mode>] [--sspl]
   511         [--macos-codesign-identity=<identity>]
   512  
   513  The mode flag accepts four values: 'local', 'slim', 'release', and
   514  'release-slim'. 'local' will build CLI and agent binaries only for the current
   515  platform. 'slim' will build the CLI binary for only the current platform and
   516  agents for a common subset of platforms. 'release' will build CLI and agent
   517  binaries for all platforms and package for release. 'release-slim' is the same
   518  as release but only builds release bundles for a small subset of platforms. The
   519  default mode is 'slim'.
   520  
   521  If --sspl is specified, then SSPL-licensed enhancements will be included in the
   522  build output. By default, only MIT-licensed code is included in builds.
   523  
   524  If --macos-codesign-identity specifies a non-empty value, then it will be used
   525  to perform code signing on all macOS binaries in a fashion suitable for
   526  notarization by Apple. The codesign utility must be able to access the
   527  associated certificate and private keys in Keychain Access without a password if
   528  this script is operated in a non-interactive mode.
   529  `
   530  
   531  // build is the primary entry point.
   532  func build() error {
   533  	// Parse command line arguments.
   534  	flagSet := pflag.NewFlagSet("build", pflag.ContinueOnError)
   535  	flagSet.SetOutput(io.Discard)
   536  	var mode, macosCodesignIdentity string
   537  	var enableSSPLEnhancements bool
   538  	flagSet.StringVarP(&mode, "mode", "m", "slim", "specify the build mode")
   539  	flagSet.StringVar(&macosCodesignIdentity, "macos-codesign-identity", "", "specify the macOS code signing identity")
   540  	flagSet.BoolVar(&enableSSPLEnhancements, "sspl", false, "enable SSPL-licensed enhancements")
   541  	if err := flagSet.Parse(os.Args[1:]); err != nil {
   542  		if err == pflag.ErrHelp {
   543  			fmt.Fprint(os.Stdout, usage)
   544  			return nil
   545  		} else {
   546  			return fmt.Errorf("unable to parse command line: %w", err)
   547  		}
   548  	}
   549  	if !(mode == "local" || mode == "slim" || mode == "release" || mode == "release-slim") {
   550  		return fmt.Errorf("invalid build mode: %s", mode)
   551  	}
   552  
   553  	// The only platform really suited to cross-compiling for every other
   554  	// platform at the moment is macOS. This is because FSEvents is used for
   555  	// file monitoring and that is a C-based API, not accessible purely via
   556  	// system calls. All of the other platforms can operate with pure Go
   557  	// compilation.
   558  	if runtime.GOOS != "darwin" {
   559  		if mode == "release" {
   560  			return errors.New("macOS is required for release builds")
   561  		} else if mode == "slim" || mode == "release-slim" {
   562  			cmd.Warning("macOS agents will be built without cgo support")
   563  		}
   564  	}
   565  
   566  	// If a macOS code signing identity has been specified, then make sure we're
   567  	// in a mode where that makes sense.
   568  	if macosCodesignIdentity != "" && runtime.GOOS != "darwin" {
   569  		return errors.New("macOS is required for macOS code signing")
   570  	}
   571  
   572  	// Compute the path to the Mutagen source directory.
   573  	mutagenSourcePath, err := mutagen.SourceTreePath()
   574  	if err != nil {
   575  		return fmt.Errorf("unable to compute Mutagen source tree path: %w", err)
   576  	}
   577  
   578  	// Verify that we're running inside the Mutagen source directory, otherwise
   579  	// we can't rely on Go modules working.
   580  	workingDirectory, err := os.Getwd()
   581  	if err != nil {
   582  		return fmt.Errorf("unable to compute working directory: %w", err)
   583  	}
   584  	workingDirectoryRelativePath, err := filepath.Rel(mutagenSourcePath, workingDirectory)
   585  	if err != nil {
   586  		return fmt.Errorf("unable to determine working directory relative path: %w", err)
   587  	}
   588  	if strings.Contains(workingDirectoryRelativePath, "..") {
   589  		return errors.New("build script run outside Mutagen source tree")
   590  	}
   591  
   592  	// Compute the path to the build directory and ensure that it exists.
   593  	buildPath := filepath.Join(mutagenSourcePath, mutagen.BuildDirectoryName)
   594  	if err := os.MkdirAll(buildPath, 0700); err != nil {
   595  		return fmt.Errorf("unable to create build directory: %w", err)
   596  	}
   597  
   598  	// Create the necessary build directory hierarchy.
   599  	agentBuildSubdirectoryPath := filepath.Join(buildPath, agentBuildSubdirectoryName)
   600  	cliBuildSubdirectoryPath := filepath.Join(buildPath, cliBuildSubdirectoryName)
   601  	releaseBuildSubdirectoryPath := filepath.Join(buildPath, releaseBuildSubdirectoryName)
   602  	if err := os.MkdirAll(agentBuildSubdirectoryPath, 0700); err != nil {
   603  		return fmt.Errorf("unable to create agent build subdirectory: %w", err)
   604  	}
   605  	if err := os.MkdirAll(cliBuildSubdirectoryPath, 0700); err != nil {
   606  		return fmt.Errorf("unable to create CLI build subdirectory: %w", err)
   607  	}
   608  	if mode == "release" || mode == "release-slim" {
   609  		if err := os.MkdirAll(releaseBuildSubdirectoryPath, 0700); err != nil {
   610  			return fmt.Errorf("unable to create release build subdirectory: %w", err)
   611  		}
   612  	}
   613  
   614  	// Compute the local target.
   615  	localTarget := Target{runtime.GOOS, runtime.GOARCH}
   616  
   617  	// Compute agent targets.
   618  	var agentTargets []Target
   619  	for _, target := range targets {
   620  		if mode == "local" && target.IsCrossTarget() {
   621  			continue
   622  		} else if (mode == "slim" || mode == "release-slim") && !target.IncludeAgentInSlimBuildModes() {
   623  			continue
   624  		}
   625  		agentTargets = append(agentTargets, target)
   626  	}
   627  
   628  	// Compute CLI targets.
   629  	var cliTargets []Target
   630  	for _, target := range targets {
   631  		if (mode == "local" || mode == "slim") && target.IsCrossTarget() {
   632  			continue
   633  		} else if mode == "release-slim" && !target.BuildBundleInReleaseSlimMode() {
   634  			continue
   635  		}
   636  		cliTargets = append(cliTargets, target)
   637  	}
   638  
   639  	// Determine whether or not to disable debugging information in binaries.
   640  	// Doing so saves significant space, but is only suited to release builds.
   641  	disableDebug := mode == "release" || mode == "release-slim"
   642  
   643  	// Build agent binaries.
   644  	log.Println("Building agent binaries...")
   645  	for _, target := range agentTargets {
   646  		log.Println("Building agent for", target)
   647  		agentBuildPath := filepath.Join(agentBuildSubdirectoryPath, target.Name())
   648  		if err := target.Build(agentPackage, agentBuildPath, enableSSPLEnhancements, disableDebug); err != nil {
   649  			return fmt.Errorf("unable to build agent: %w", err)
   650  		}
   651  		if macosCodesignIdentity != "" && target.GOOS == "darwin" {
   652  			if err := macOSCodeSign(agentBuildPath, macosCodesignIdentity); err != nil {
   653  				return fmt.Errorf("unable to code sign agent for macOS: %w", err)
   654  			}
   655  		}
   656  	}
   657  
   658  	// Build CLI binaries.
   659  	log.Println("Building CLI binaries...")
   660  	for _, target := range cliTargets {
   661  		log.Println("Building CLI for", target)
   662  		cliBuildPath := filepath.Join(cliBuildSubdirectoryPath, target.Name())
   663  		if err := target.Build(cliPackage, cliBuildPath, enableSSPLEnhancements, disableDebug); err != nil {
   664  			return fmt.Errorf("unable to build CLI: %w", err)
   665  		}
   666  		if macosCodesignIdentity != "" && target.GOOS == "darwin" {
   667  			if err := macOSCodeSign(cliBuildPath, macosCodesignIdentity); err != nil {
   668  				return fmt.Errorf("unable to code sign CLI for macOS: %w", err)
   669  			}
   670  		}
   671  	}
   672  
   673  	// Build the agent bundle.
   674  	log.Println("Building agent bundle...")
   675  	agentBundlePath := filepath.Join(buildPath, agent.BundleName)
   676  	agentBundleBuilder, err := NewArchiveBuilder(agentBundlePath)
   677  	if err != nil {
   678  		return fmt.Errorf("unable to create agent bundle archive builder: %w", err)
   679  	}
   680  	for _, target := range agentTargets {
   681  		agentBuildPath := filepath.Join(agentBuildSubdirectoryPath, target.Name())
   682  		if err := agentBundleBuilder.Add(target.Name(), agentBuildPath, 0755); err != nil {
   683  			agentBundleBuilder.Close()
   684  			return fmt.Errorf("unable to add agent to bundle: %w", err)
   685  		}
   686  	}
   687  	if err := agentBundleBuilder.Close(); err != nil {
   688  		return fmt.Errorf("unable to finalize agent bundle: %w", err)
   689  	}
   690  
   691  	// Build release bundles if necessary.
   692  	if mode == "release" || mode == "release-slim" {
   693  		log.Println("Building release bundles...")
   694  		for _, target := range cliTargets {
   695  			// Update status.
   696  			log.Println("Building release bundle for", target)
   697  
   698  			// Compute paths.
   699  			cliBuildPath := filepath.Join(cliBuildSubdirectoryPath, target.Name())
   700  			releaseBundlePath := filepath.Join(
   701  				releaseBuildSubdirectoryPath,
   702  				fmt.Sprintf("mutagen_%s_v%s.tar.gz", target.Name(), mutagen.Version),
   703  			)
   704  
   705  			// Build the release bundle.
   706  			if releaseBundle, err := NewArchiveBuilder(releaseBundlePath); err != nil {
   707  				return fmt.Errorf("unable to create release bundle: %w", err)
   708  			} else if err = releaseBundle.Add(target.ExecutableName(cliBaseName), cliBuildPath, 0755); err != nil {
   709  				releaseBundle.Close()
   710  				return fmt.Errorf("unable to add CLI to release bundle: %w", err)
   711  			} else if err = releaseBundle.Add("", agentBundlePath, 0644); err != nil {
   712  				releaseBundle.Close()
   713  				return fmt.Errorf("unable to add agent bundle to release bundle: %w", err)
   714  			} else if err = releaseBundle.Close(); err != nil {
   715  				return fmt.Errorf("unable to finalize release bundle: %w", err)
   716  			}
   717  		}
   718  	}
   719  
   720  	// Relocate the CLI binary for the current platform.
   721  	log.Println("Copying binary for testing")
   722  	localCLIBuildPath := filepath.Join(cliBuildSubdirectoryPath, localTarget.Name())
   723  	localCLIRelocationPath := filepath.Join(buildPath, localTarget.ExecutableName(cliBaseName))
   724  	if err := copyFile(localCLIBuildPath, localCLIRelocationPath); err != nil {
   725  		return fmt.Errorf("unable to copy current platform CLI: %w", err)
   726  	}
   727  
   728  	// Success.
   729  	return nil
   730  }
   731  
   732  func main() {
   733  	if err := build(); err != nil {
   734  		cmd.Fatal(err)
   735  	}
   736  }