github.com/artpar/rclone@v1.67.3/bin/cross-compile.go (about)

     1  //go:build ignore
     2  
     3  // Cross compile rclone - in go because I hate bash ;-)
     4  
     5  package main
     6  
     7  import (
     8  	"flag"
     9  	"fmt"
    10  	"log"
    11  	"os"
    12  	"os/exec"
    13  	"path"
    14  	"path/filepath"
    15  	"regexp"
    16  	"runtime"
    17  	"sort"
    18  	"strings"
    19  	"sync"
    20  	"text/template"
    21  	"time"
    22  )
    23  
    24  var (
    25  	// Flags
    26  	debug           = flag.Bool("d", false, "Print commands instead of running them")
    27  	parallel        = flag.Int("parallel", runtime.NumCPU(), "Number of commands to run in parallel")
    28  	copyAs          = flag.String("release", "", "Make copies of the releases with this name")
    29  	gitLog          = flag.String("git-log", "", "git log to include as well")
    30  	include         = flag.String("include", "^.*$", "os/arch regexp to include")
    31  	exclude         = flag.String("exclude", "^$", "os/arch regexp to exclude")
    32  	cgo             = flag.Bool("cgo", false, "Use cgo for the build")
    33  	noClean         = flag.Bool("no-clean", false, "Don't clean the build directory before running")
    34  	tags            = flag.String("tags", "", "Space separated list of build tags")
    35  	buildmode       = flag.String("buildmode", "", "Passed to go build -buildmode flag")
    36  	compileOnly     = flag.Bool("compile-only", false, "Just build the binary, not the zip")
    37  	extraEnv        = flag.String("env", "", "comma separated list of VAR=VALUE env vars to set")
    38  	macOSSDK        = flag.String("macos-sdk", "", "macOS SDK to use")
    39  	macOSArch       = flag.String("macos-arch", "", "macOS arch to use")
    40  	extraCgoCFlags  = flag.String("cgo-cflags", "", "extra CGO_CFLAGS")
    41  	extraCgoLdFlags = flag.String("cgo-ldflags", "", "extra CGO_LDFLAGS")
    42  )
    43  
    44  // GOOS/GOARCH pairs we build for
    45  //
    46  // If the GOARCH contains a - it is a synthetic arch with more parameters
    47  var osarches = []string{
    48  	"windows/386",
    49  	"windows/amd64",
    50  	"windows/arm64",
    51  	"darwin/amd64",
    52  	"darwin/arm64",
    53  	"linux/386",
    54  	"linux/amd64",
    55  	"linux/arm",
    56  	"linux/arm-v6",
    57  	"linux/arm-v7",
    58  	"linux/arm64",
    59  	"linux/mips",
    60  	"linux/mipsle",
    61  	"freebsd/386",
    62  	"freebsd/amd64",
    63  	"freebsd/arm",
    64  	"freebsd/arm-v6",
    65  	"freebsd/arm-v7",
    66  	"netbsd/386",
    67  	"netbsd/amd64",
    68  	"netbsd/arm",
    69  	"netbsd/arm-v6",
    70  	"netbsd/arm-v7",
    71  	"openbsd/386",
    72  	"openbsd/amd64",
    73  	"plan9/386",
    74  	"plan9/amd64",
    75  	"solaris/amd64",
    76  	"js/wasm",
    77  }
    78  
    79  // Special environment flags for a given arch
    80  var archFlags = map[string][]string{
    81  	"386":    {"GO386=softfloat"},
    82  	"mips":   {"GOMIPS=softfloat"},
    83  	"mipsle": {"GOMIPS=softfloat"},
    84  	"arm":    {"GOARM=5"},
    85  	"arm-v6": {"GOARM=6"},
    86  	"arm-v7": {"GOARM=7"},
    87  }
    88  
    89  // Map Go architectures to NFPM architectures
    90  // Any missing are passed straight through
    91  var goarchToNfpm = map[string]string{
    92  	"arm":    "arm5",
    93  	"arm-v6": "arm6",
    94  	"arm-v7": "arm7",
    95  }
    96  
    97  // runEnv - run a shell command with env
    98  func runEnv(args, env []string) error {
    99  	if *debug {
   100  		args = append([]string{"echo"}, args...)
   101  	}
   102  	cmd := exec.Command(args[0], args[1:]...)
   103  	if env != nil {
   104  		cmd.Env = append(os.Environ(), env...)
   105  	}
   106  	if *debug {
   107  		log.Printf("args = %v, env = %v\n", args, cmd.Env)
   108  	}
   109  	out, err := cmd.CombinedOutput()
   110  	if err != nil {
   111  		log.Print("----------------------------")
   112  		log.Printf("Failed to run %v: %v", args, err)
   113  		log.Printf("Command output was:\n%s", out)
   114  		log.Print("----------------------------")
   115  	}
   116  	return err
   117  }
   118  
   119  // run a shell command
   120  func run(args ...string) {
   121  	err := runEnv(args, nil)
   122  	if err != nil {
   123  		log.Fatalf("Exiting after error: %v", err)
   124  	}
   125  }
   126  
   127  // chdir or die
   128  func chdir(dir string) {
   129  	err := os.Chdir(dir)
   130  	if err != nil {
   131  		log.Fatalf("Couldn't cd into %q: %v", dir, err)
   132  	}
   133  }
   134  
   135  // substitute data from go template file in to file out
   136  func substitute(inFile, outFile string, data interface{}) {
   137  	t, err := template.ParseFiles(inFile)
   138  	if err != nil {
   139  		log.Fatalf("Failed to read template file %q: %v", inFile, err)
   140  	}
   141  	out, err := os.Create(outFile)
   142  	if err != nil {
   143  		log.Fatalf("Failed to create output file %q: %v", outFile, err)
   144  	}
   145  	defer func() {
   146  		err := out.Close()
   147  		if err != nil {
   148  			log.Fatalf("Failed to close output file %q: %v", outFile, err)
   149  		}
   150  	}()
   151  	err = t.Execute(out, data)
   152  	if err != nil {
   153  		log.Fatalf("Failed to substitute template file %q: %v", inFile, err)
   154  	}
   155  }
   156  
   157  // build the zip package return its name
   158  func buildZip(dir string) string {
   159  	// Now build the zip
   160  	run("cp", "-a", "../MANUAL.txt", filepath.Join(dir, "README.txt"))
   161  	run("cp", "-a", "../MANUAL.html", filepath.Join(dir, "README.html"))
   162  	run("cp", "-a", "../rclone.1", dir)
   163  	if *gitLog != "" {
   164  		run("cp", "-a", *gitLog, dir)
   165  	}
   166  	zip := dir + ".zip"
   167  	run("zip", "-r9", zip, dir)
   168  	return zip
   169  }
   170  
   171  // Build .deb and .rpm packages
   172  //
   173  // It returns a list of artifacts it has made
   174  func buildDebAndRpm(dir, version, goarch string) []string {
   175  	// Make internal version number acceptable to .deb and .rpm
   176  	pkgVersion := version[1:]
   177  	pkgVersion = strings.ReplaceAll(pkgVersion, "β", "-beta")
   178  	pkgVersion = strings.ReplaceAll(pkgVersion, "-", ".")
   179  	nfpmArch, ok := goarchToNfpm[goarch]
   180  	if !ok {
   181  		nfpmArch = goarch
   182  	}
   183  
   184  	// Make nfpm.yaml from the template
   185  	substitute("../bin/nfpm.yaml", path.Join(dir, "nfpm.yaml"), map[string]string{
   186  		"Version": pkgVersion,
   187  		"Arch":    nfpmArch,
   188  	})
   189  
   190  	// build them
   191  	var artifacts []string
   192  	for _, pkg := range []string{".deb", ".rpm"} {
   193  		artifact := dir + pkg
   194  		run("bash", "-c", "cd "+dir+" && nfpm -f nfpm.yaml pkg -t ../"+artifact)
   195  		artifacts = append(artifacts, artifact)
   196  	}
   197  
   198  	return artifacts
   199  }
   200  
   201  // Trip a version suffix off the arch if present
   202  func stripVersion(goarch string) string {
   203  	i := strings.Index(goarch, "-")
   204  	if i < 0 {
   205  		return goarch
   206  	}
   207  	return goarch[:i]
   208  }
   209  
   210  // run the command returning trimmed output
   211  func runOut(command ...string) string {
   212  	out, err := exec.Command(command[0], command[1:]...).Output()
   213  	if err != nil {
   214  		log.Fatalf("Failed to run %q: %v", command, err)
   215  	}
   216  	return strings.TrimSpace(string(out))
   217  }
   218  
   219  // Generate Windows resource system object file (.syso), which can be picked
   220  // up by the following go build for embedding version information and icon
   221  // resources into the executable.
   222  func generateResourceWindows(version, arch string) func() {
   223  	sysoPath := fmt.Sprintf("../resource_windows_%s.syso", arch) // Use explicit destination filename, even though it should be same as default, so that we are sure we have the correct reference to it
   224  	if err := os.Remove(sysoPath); !os.IsNotExist(err) {
   225  		// Note: This one we choose to treat as fatal, to avoid any risk of picking up an old .syso file without noticing.
   226  		log.Fatalf("Failed to remove existing Windows %s resource system object file %s: %v", arch, sysoPath, err)
   227  	}
   228  	args := []string{"go", "run", "../bin/resource_windows.go", "-arch", arch, "-version", version, "-syso", sysoPath}
   229  	if err := runEnv(args, nil); err != nil {
   230  		log.Printf("Warning: Couldn't generate Windows %s resource system object file, binaries will not have version information or icon embedded", arch)
   231  		return nil
   232  	}
   233  	if _, err := os.Stat(sysoPath); err != nil {
   234  		log.Printf("Warning: Couldn't find generated Windows %s resource system object file, binaries will not have version information or icon embedded", arch)
   235  		return nil
   236  	}
   237  	return func() {
   238  		if err := os.Remove(sysoPath); err != nil && !os.IsNotExist(err) {
   239  			log.Printf("Warning: Couldn't remove generated Windows %s resource system object file %s: %v. Please remove it manually.", arch, sysoPath, err)
   240  		}
   241  	}
   242  }
   243  
   244  // build the binary in dir returning success or failure
   245  func compileArch(version, goos, goarch, dir string) bool {
   246  	log.Printf("Compiling %s/%s into %s", goos, goarch, dir)
   247  	goarchBase := stripVersion(goarch)
   248  	output := filepath.Join(dir, "rclone")
   249  	if goos == "windows" {
   250  		output += ".exe"
   251  		if cleanupFn := generateResourceWindows(version, goarchBase); cleanupFn != nil {
   252  			defer cleanupFn()
   253  		}
   254  	}
   255  	err := os.MkdirAll(dir, 0777)
   256  	if err != nil {
   257  		log.Fatalf("Failed to mkdir: %v", err)
   258  	}
   259  	args := []string{
   260  		"go", "build",
   261  		"--ldflags", "-s -X github.com/artpar/rclone/fs.Version=" + version,
   262  		"-trimpath",
   263  		"-o", output,
   264  		"-tags", *tags,
   265  	}
   266  	if *buildmode != "" {
   267  		args = append(args,
   268  			"-buildmode", *buildmode,
   269  		)
   270  	}
   271  	args = append(args,
   272  		"..",
   273  	)
   274  	env := []string{
   275  		"GOOS=" + goos,
   276  		"GOARCH=" + goarchBase,
   277  	}
   278  	if *extraEnv != "" {
   279  		env = append(env, strings.Split(*extraEnv, ",")...)
   280  	}
   281  	var (
   282  		cgoCFlags  []string
   283  		cgoLdFlags []string
   284  	)
   285  	if *macOSSDK != "" {
   286  		flag := "-isysroot " + runOut("xcrun", "--sdk", *macOSSDK, "--show-sdk-path")
   287  		cgoCFlags = append(cgoCFlags, flag)
   288  		cgoLdFlags = append(cgoLdFlags, flag)
   289  	}
   290  	if *macOSArch != "" {
   291  		flag := "-arch " + *macOSArch
   292  		cgoCFlags = append(cgoCFlags, flag)
   293  		cgoLdFlags = append(cgoLdFlags, flag)
   294  	}
   295  	if *extraCgoCFlags != "" {
   296  		cgoCFlags = append(cgoCFlags, *extraCgoCFlags)
   297  	}
   298  	if *extraCgoLdFlags != "" {
   299  		cgoLdFlags = append(cgoLdFlags, *extraCgoLdFlags)
   300  	}
   301  	if len(cgoCFlags) > 0 {
   302  		env = append(env, "CGO_CFLAGS="+strings.Join(cgoCFlags, " "))
   303  	}
   304  	if len(cgoLdFlags) > 0 {
   305  		env = append(env, "CGO_LDFLAGS="+strings.Join(cgoLdFlags, " "))
   306  	}
   307  	if !*cgo {
   308  		env = append(env, "CGO_ENABLED=0")
   309  	} else {
   310  		env = append(env, "CGO_ENABLED=1")
   311  	}
   312  	if flags, ok := archFlags[goarch]; ok {
   313  		env = append(env, flags...)
   314  	}
   315  	err = runEnv(args, env)
   316  	if err != nil {
   317  		log.Printf("Error compiling %s/%s: %v", goos, goarch, err)
   318  		return false
   319  	}
   320  	if !*compileOnly {
   321  		if goos != "js" {
   322  			artifacts := []string{buildZip(dir)}
   323  			// build a .deb and .rpm if appropriate
   324  			if goos == "linux" {
   325  				artifacts = append(artifacts, buildDebAndRpm(dir, version, goarch)...)
   326  			}
   327  			if *copyAs != "" {
   328  				for _, artifact := range artifacts {
   329  					run("ln", artifact, strings.Replace(artifact, "-"+version, "-"+*copyAs, 1))
   330  				}
   331  			}
   332  		}
   333  		// tidy up
   334  		run("rm", "-rf", dir)
   335  	}
   336  	log.Printf("Done compiling %s/%s", goos, goarch)
   337  	return true
   338  }
   339  
   340  func compile(version string) {
   341  	start := time.Now()
   342  	wg := new(sync.WaitGroup)
   343  	run := make(chan func(), *parallel)
   344  	for i := 0; i < *parallel; i++ {
   345  		wg.Add(1)
   346  		go func() {
   347  			defer wg.Done()
   348  			for f := range run {
   349  				f()
   350  			}
   351  		}()
   352  	}
   353  	includeRe, err := regexp.Compile(*include)
   354  	if err != nil {
   355  		log.Fatalf("Bad -include regexp: %v", err)
   356  	}
   357  	excludeRe, err := regexp.Compile(*exclude)
   358  	if err != nil {
   359  		log.Fatalf("Bad -exclude regexp: %v", err)
   360  	}
   361  	compiled := 0
   362  	var failuresMu sync.Mutex
   363  	var failures []string
   364  	for _, osarch := range osarches {
   365  		if excludeRe.MatchString(osarch) || !includeRe.MatchString(osarch) {
   366  			continue
   367  		}
   368  		parts := strings.Split(osarch, "/")
   369  		if len(parts) != 2 {
   370  			log.Fatalf("Bad osarch %q", osarch)
   371  		}
   372  		goos, goarch := parts[0], parts[1]
   373  		userGoos := goos
   374  		if goos == "darwin" {
   375  			userGoos = "osx"
   376  		}
   377  		dir := filepath.Join("rclone-" + version + "-" + userGoos + "-" + goarch)
   378  		run <- func() {
   379  			if !compileArch(version, goos, goarch, dir) {
   380  				failuresMu.Lock()
   381  				failures = append(failures, goos+"/"+goarch)
   382  				failuresMu.Unlock()
   383  			}
   384  		}
   385  		compiled++
   386  	}
   387  	close(run)
   388  	wg.Wait()
   389  	log.Printf("Compiled %d arches in %v", compiled, time.Since(start))
   390  	if len(failures) > 0 {
   391  		sort.Strings(failures)
   392  		log.Printf("%d compile failures:\n  %s\n", len(failures), strings.Join(failures, "\n  "))
   393  		os.Exit(1)
   394  	}
   395  }
   396  
   397  func main() {
   398  	flag.Parse()
   399  	args := flag.Args()
   400  	if len(args) != 1 {
   401  		log.Fatalf("Syntax: %s <version>", os.Args[0])
   402  	}
   403  	version := args[0]
   404  	if !*noClean {
   405  		run("rm", "-rf", "build")
   406  		run("mkdir", "build")
   407  	}
   408  	chdir("build")
   409  	err := os.WriteFile("version.txt", []byte(fmt.Sprintf("rclone %s\n", version)), 0666)
   410  	if err != nil {
   411  		log.Fatalf("Couldn't write version.txt: %v", err)
   412  	}
   413  	compile(version)
   414  }