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 }